├── .editorconfig
├── .gitignore
├── EZBlocker3.sln
├── EZBlocker3
├── App.xaml
├── App.xaml.cs
├── AssemblyInfo.cs
├── Audio
│ ├── CoreAudio
│ │ ├── AudioDevice.cs
│ │ ├── AudioSession.cs
│ │ ├── AudioSessionCollection.cs
│ │ └── AudioSessionManager.cs
│ └── VolumeMixer.cs
├── AutoUpdate
│ ├── DownloadUpdateWindow.xaml
│ ├── DownloadUpdateWindow.xaml.cs
│ ├── DownloadedUpdate.cs
│ ├── UpdateChecker.cs
│ ├── UpdateDownloader.cs
│ ├── UpdateFoundWindow.xaml
│ ├── UpdateFoundWindow.xaml.cs
│ ├── UpdateInfo.cs
│ └── UpdateInstaller.cs
├── CliArgs.cs
├── EZBlocker3.csproj
├── Extensions
│ ├── ArrayExtensions.cs
│ ├── DirectoryInfoExtensions.cs
│ ├── DispatcherExtensions.cs
│ ├── IEnumerableExtensions.cs
│ ├── KeyValuePairExtensions.cs
│ ├── PointExtensions.cs
│ ├── ProcessExtensions.cs
│ ├── QueueExtensions.cs
│ ├── SizeExtensions.cs
│ ├── StreamExtensions.cs
│ ├── StringExtensions.cs
│ └── TypeExtensions.cs
├── FodyWeavers.xml
├── GlobalSingletons.cs
├── Icon
│ ├── Icon.ai
│ ├── Icon128.ico
│ ├── Icon128.png
│ ├── Icon16.ico
│ ├── Icon16.png
│ ├── Icon256.ico
│ ├── Icon256.png
│ ├── Icon32.ico
│ ├── Icon32.png
│ ├── Icon64.ico
│ └── Icon64.png
├── IllegalStateException.cs
├── ImageDictionary.xaml
├── Interop
│ ├── NativeUtils.cs
│ └── PInvokeExtra.cs
├── IsExternalInit.cs
├── Logging
│ ├── LogLevel.cs
│ ├── Logger.cs
│ └── NamedLogger.cs
├── MainWindow.xaml
├── MainWindow.xaml.cs
├── Program.cs
├── Properties
│ ├── Resources.Designer.cs
│ ├── Resources.resx
│ ├── Settings.Designer.cs
│ └── Settings.settings
├── Settings
│ ├── Autostart.cs
│ ├── SettingsWindow.xaml
│ ├── SettingsWindow.xaml.cs
│ ├── StartWithSpotify.cs
│ └── Uninstall.cs
├── Spotify
│ ├── AbstractSpotifyAdBlocker.cs
│ ├── AbstractSpotifyHook.cs
│ ├── GlobalSystemMediaTransportControlSpotifyHook.cs
│ ├── IActivatable.cs
│ ├── IMutingSpotifyHook.cs
│ ├── ISpotifyHook.cs
│ ├── MutingSpotifyAdBlocker.cs
│ ├── ProcessAndWindowEventSpotifyHook.cs
│ ├── SkippingSpotifyAdBlocker.cs
│ ├── SongInfo.cs
│ ├── SpotifyHandler.cs
│ ├── SpotifyState.cs
│ └── SpotifyUtils.cs
├── Utils
│ ├── BitmapUtils.cs
│ ├── DisposableList.cs
│ ├── KeyDisposableDictionary.cs
│ ├── KeyValueDisposableDictionary.cs
│ ├── ValueDisposableDictionary.cs
│ └── WindowHelper.cs
└── app.manifest
├── Interop
├── Interop.csproj
├── NativeMethods.json
└── NativeMethods.txt
├── LICENSE
├── README.md
└── screenshots
├── screenshot-app-dark.png
└── screenshot-app-light.png
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # U2U1012: Parameter types should be specific
4 | dotnet_diagnostic.U2U1012.severity = none
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # ASP.NET Scaffolding
66 | ScaffoldingReadMe.txt
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.vspscc
94 | *.vssscc
95 | .builds
96 | *.pidb
97 | *.svclog
98 | *.scc
99 |
100 | # Chutzpah Test files
101 | _Chutzpah*
102 |
103 | # Visual C++ cache files
104 | ipch/
105 | *.aps
106 | *.ncb
107 | *.opendb
108 | *.opensdf
109 | *.sdf
110 | *.cachefile
111 | *.VC.db
112 | *.VC.VC.opendb
113 |
114 | # Visual Studio profiler
115 | *.psess
116 | *.vsp
117 | *.vspx
118 | *.sap
119 |
120 | # Visual Studio Trace Files
121 | *.e2e
122 |
123 | # TFS 2012 Local Workspace
124 | $tf/
125 |
126 | # Guidance Automation Toolkit
127 | *.gpState
128 |
129 | # ReSharper is a .NET coding add-in
130 | _ReSharper*/
131 | *.[Rr]e[Ss]harper
132 | *.DotSettings.user
133 |
134 | # TeamCity is a build add-in
135 | _TeamCity*
136 |
137 | # DotCover is a Code Coverage Tool
138 | *.dotCover
139 |
140 | # AxoCover is a Code Coverage Tool
141 | .axoCover/*
142 | !.axoCover/settings.json
143 |
144 | # Coverlet is a free, cross platform Code Coverage Tool
145 | coverage*.json
146 | coverage*.xml
147 | coverage*.info
148 |
149 | # Visual Studio code coverage results
150 | *.coverage
151 | *.coveragexml
152 |
153 | # NCrunch
154 | _NCrunch_*
155 | .*crunch*.local.xml
156 | nCrunchTemp_*
157 |
158 | # MightyMoose
159 | *.mm.*
160 | AutoTest.Net/
161 |
162 | # Web workbench (sass)
163 | .sass-cache/
164 |
165 | # Installshield output folder
166 | [Ee]xpress/
167 |
168 | # DocProject is a documentation generator add-in
169 | DocProject/buildhelp/
170 | DocProject/Help/*.HxT
171 | DocProject/Help/*.HxC
172 | DocProject/Help/*.hhc
173 | DocProject/Help/*.hhk
174 | DocProject/Help/*.hhp
175 | DocProject/Help/Html2
176 | DocProject/Help/html
177 |
178 | # Click-Once directory
179 | publish/
180 |
181 | # Publish Web Output
182 | *.[Pp]ublish.xml
183 | *.azurePubxml
184 | # Note: Comment the next line if you want to checkin your web deploy settings,
185 | # but database connection strings (with potential passwords) will be unencrypted
186 | *.pubxml
187 | *.publishproj
188 |
189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
190 | # checkin your Azure Web App publish settings, but sensitive information contained
191 | # in these scripts will be unencrypted
192 | PublishScripts/
193 |
194 | # NuGet Packages
195 | *.nupkg
196 | # NuGet Symbol Packages
197 | *.snupkg
198 | # The packages folder can be ignored because of Package Restore
199 | **/[Pp]ackages/*
200 | # except build/, which is used as an MSBuild target.
201 | !**/[Pp]ackages/build/
202 | # Uncomment if necessary however generally it will be regenerated when needed
203 | #!**/[Pp]ackages/repositories.config
204 | # NuGet v3's project.json files produces more ignorable files
205 | *.nuget.props
206 | *.nuget.targets
207 |
208 | # Microsoft Azure Build Output
209 | csx/
210 | *.build.csdef
211 |
212 | # Microsoft Azure Emulator
213 | ecf/
214 | rcf/
215 |
216 | # Windows Store app package directories and files
217 | AppPackages/
218 | BundleArtifacts/
219 | Package.StoreAssociation.xml
220 | _pkginfo.txt
221 | *.appx
222 | *.appxbundle
223 | *.appxupload
224 |
225 | # Visual Studio cache files
226 | # files ending in .cache can be ignored
227 | *.[Cc]ache
228 | # but keep track of directories ending in .cache
229 | !?*.[Cc]ache/
230 |
231 | # Others
232 | ClientBin/
233 | ~$*
234 | *~
235 | *.dbmdl
236 | *.dbproj.schemaview
237 | *.jfm
238 | *.pfx
239 | *.publishsettings
240 | orleans.codegen.cs
241 |
242 | # Including strong name files can present a security risk
243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
244 | #*.snk
245 |
246 | # Since there are multiple workflows, uncomment next line to ignore bower_components
247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
248 | #bower_components/
249 |
250 | # RIA/Silverlight projects
251 | Generated_Code/
252 |
253 | # Backup & report files from converting an old project file
254 | # to a newer Visual Studio version. Backup files are not needed,
255 | # because we have git ;-)
256 | _UpgradeReport_Files/
257 | Backup*/
258 | UpgradeLog*.XML
259 | UpgradeLog*.htm
260 | ServiceFabricBackup/
261 | *.rptproj.bak
262 |
263 | # SQL Server files
264 | *.mdf
265 | *.ldf
266 | *.ndf
267 |
268 | # Business Intelligence projects
269 | *.rdl.data
270 | *.bim.layout
271 | *.bim_*.settings
272 | *.rptproj.rsuser
273 | *- [Bb]ackup.rdl
274 | *- [Bb]ackup ([0-9]).rdl
275 | *- [Bb]ackup ([0-9][0-9]).rdl
276 |
277 | # Microsoft Fakes
278 | FakesAssemblies/
279 |
280 | # GhostDoc plugin setting file
281 | *.GhostDoc.xml
282 |
283 | # Node.js Tools for Visual Studio
284 | .ntvs_analysis.dat
285 | node_modules/
286 |
287 | # Visual Studio 6 build log
288 | *.plg
289 |
290 | # Visual Studio 6 workspace options file
291 | *.opt
292 |
293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
294 | *.vbw
295 |
296 | # Visual Studio LightSwitch build output
297 | **/*.HTMLClient/GeneratedArtifacts
298 | **/*.DesktopClient/GeneratedArtifacts
299 | **/*.DesktopClient/ModelManifest.xml
300 | **/*.Server/GeneratedArtifacts
301 | **/*.Server/ModelManifest.xml
302 | _Pvt_Extensions
303 |
304 | # Paket dependency manager
305 | .paket/paket.exe
306 | paket-files/
307 |
308 | # FAKE - F# Make
309 | .fake/
310 |
311 | # CodeRush personal settings
312 | .cr/personal
313 |
314 | # Python Tools for Visual Studio (PTVS)
315 | __pycache__/
316 | *.pyc
317 |
318 | # Cake - Uncomment if you are using it
319 | # tools/**
320 | # !tools/packages.config
321 |
322 | # Tabs Studio
323 | *.tss
324 |
325 | # Telerik's JustMock configuration file
326 | *.jmconfig
327 |
328 | # BizTalk build output
329 | *.btp.cs
330 | *.btm.cs
331 | *.odx.cs
332 | *.xsd.cs
333 |
334 | # OpenCover UI analysis results
335 | OpenCover/
336 |
337 | # Azure Stream Analytics local run output
338 | ASALocalRun/
339 |
340 | # MSBuild Binary and Structured Log
341 | *.binlog
342 |
343 | # NVidia Nsight GPU debugger configuration file
344 | *.nvuser
345 |
346 | # MFractors (Xamarin productivity tool) working folder
347 | .mfractor/
348 |
349 | # Local History for Visual Studio
350 | .localhistory/
351 |
352 | # BeatPulse healthcheck temp database
353 | healthchecksdb
354 |
355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
356 | MigrationBackup/
357 |
358 | # Ionide (cross platform F# VS Code tools) working folder
359 | .ionide/
360 |
361 | # Fody - auto-generated XML schema
362 | FodyWeavers.xsd
363 |
--------------------------------------------------------------------------------
/EZBlocker3.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.30503.244
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EZBlocker3", "EZBlocker3\EZBlocker3.csproj", "{BEBA52C5-91DB-4597-9ED0-C95FA33D9CFB}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Interop", "Interop\Interop.csproj", "{A16469B9-FB64-4B0A-AC62-E69DDD103068}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{EDAC5A89-F78E-4A8C-B57A-B08830753CA4}"
11 | ProjectSection(SolutionItems) = preProject
12 | .editorconfig = .editorconfig
13 | EndProjectSection
14 | EndProject
15 | Global
16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
17 | Debug|Any CPU = Debug|Any CPU
18 | Debug|x64 = Debug|x64
19 | Release|Any CPU = Release|Any CPU
20 | Release|x64 = Release|x64
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {BEBA52C5-91DB-4597-9ED0-C95FA33D9CFB}.Debug|Any CPU.ActiveCfg = Debug|x64
24 | {BEBA52C5-91DB-4597-9ED0-C95FA33D9CFB}.Debug|Any CPU.Build.0 = Debug|x64
25 | {BEBA52C5-91DB-4597-9ED0-C95FA33D9CFB}.Debug|x64.ActiveCfg = Debug|x64
26 | {BEBA52C5-91DB-4597-9ED0-C95FA33D9CFB}.Debug|x64.Build.0 = Debug|x64
27 | {BEBA52C5-91DB-4597-9ED0-C95FA33D9CFB}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {BEBA52C5-91DB-4597-9ED0-C95FA33D9CFB}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {BEBA52C5-91DB-4597-9ED0-C95FA33D9CFB}.Release|x64.ActiveCfg = Release|x64
30 | {BEBA52C5-91DB-4597-9ED0-C95FA33D9CFB}.Release|x64.Build.0 = Release|x64
31 | {A16469B9-FB64-4B0A-AC62-E69DDD103068}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32 | {A16469B9-FB64-4B0A-AC62-E69DDD103068}.Debug|Any CPU.Build.0 = Debug|Any CPU
33 | {A16469B9-FB64-4B0A-AC62-E69DDD103068}.Debug|x64.ActiveCfg = Debug|Any CPU
34 | {A16469B9-FB64-4B0A-AC62-E69DDD103068}.Debug|x64.Build.0 = Debug|Any CPU
35 | {A16469B9-FB64-4B0A-AC62-E69DDD103068}.Release|Any CPU.ActiveCfg = Release|Any CPU
36 | {A16469B9-FB64-4B0A-AC62-E69DDD103068}.Release|Any CPU.Build.0 = Release|Any CPU
37 | {A16469B9-FB64-4B0A-AC62-E69DDD103068}.Release|x64.ActiveCfg = Release|Any CPU
38 | {A16469B9-FB64-4B0A-AC62-E69DDD103068}.Release|x64.Build.0 = Release|Any CPU
39 | EndGlobalSection
40 | GlobalSection(SolutionProperties) = preSolution
41 | HideSolutionNode = FALSE
42 | EndGlobalSection
43 | GlobalSection(ExtensibilityGlobals) = postSolution
44 | SolutionGuid = {D23630D6-230B-44F0-85EE-B19536266F12}
45 | EndGlobalSection
46 | EndGlobal
47 |
--------------------------------------------------------------------------------
/EZBlocker3/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/EZBlocker3/App.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Net;
4 | using System.Reflection;
5 | using System.Threading.Tasks;
6 | using System.Windows;
7 | using EZBlocker3.Logging;
8 | using EZBlocker3.Settings;
9 |
10 | namespace EZBlocker3 {
11 | public partial class App : Application {
12 | public static readonly Assembly Assembly = Assembly.GetExecutingAssembly();
13 | public static readonly AssemblyName AssemblyName = Assembly.GetName();
14 | public static readonly string Name = AssemblyName.Name;
15 | public static readonly string ProductName = Assembly.GetCustomAttribute().Product;
16 | public static readonly string CompanyName = Assembly.GetCustomAttribute().Company;
17 | public static readonly string Location = Assembly.Location;
18 | public static readonly string Directory = Path.GetDirectoryName(Location);
19 | public static readonly Version Version = AssemblyName.Version;
20 |
21 | private const bool IsDebugBuild =
22 | #if DEBUG
23 | true;
24 | #else
25 | false;
26 | # endif
27 | internal static bool ForceDebugMode = false;
28 | public static bool DebugModeEnabled => IsDebugBuild || ForceDebugMode || EZBlocker3.Properties.Settings.Default.DebugMode;
29 | public static readonly bool ForceUpdate = false;
30 | public static readonly bool ForceUpdateCheck = IsDebugBuild || ForceUpdate;
31 | internal static bool SaveSettingsOnClose = true;
32 |
33 | protected override void OnStartup(StartupEventArgs eventArgs) {
34 | base.OnStartup(eventArgs);
35 |
36 | // enable all security protocols
37 | // without this statement https requests fail.
38 | ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
39 |
40 | // reenable settings after update (disabled in UpdateInstaller.InstallUpdateAndRestart)
41 | var settings = EZBlocker3.Properties.Settings.Default;
42 | if (settings.UpgradeRequired || Program.CliArgs.IsUpdateRestart) {
43 | if (StartWithSpotify.Available)
44 | StartWithSpotify.SetEnabled(settings.StartWithSpotify);
45 | Autostart.SetEnabled(settings.StartOnLogin);
46 | }
47 |
48 | // upgrade settings on first start (after update)
49 | if (settings.UpgradeRequired) {
50 | settings.Upgrade();
51 | settings.UpgradeRequired = false;
52 | settings.Save();
53 | }
54 |
55 | // check if executable has moved
56 | if (Location != settings.AppPath) {
57 | try {
58 | if (settings.StartOnLogin)
59 | Autostart.SetEnabled(settings.StartOnLogin);
60 | // StartWithSpotify.SetEnabled(settings.StartWithSpotify);
61 | } catch (Exception e) {
62 | Logger.LogException("Failed to adjust to changed app path:", e);
63 | }
64 |
65 | settings.AppPath = Location;
66 | }
67 |
68 | if (settings.StartWithSpotify) {
69 | Task.Run(static () => {
70 | // Ensure that the proxy is still installed correctly if enabled.
71 | StartWithSpotify.Enable();
72 |
73 | // start spotify if start with spotify is enabled but we did not start through the proxy
74 | // if (!Program.CliArgs.IsProxyStart) {
75 | // StartWithSpotify.TransformToProxied();
76 | // StartWithSpotify.StartSpotify();
77 | // }
78 | });
79 | }
80 |
81 | // create main window
82 | var mainWindow = new MainWindow();
83 | if (EZBlocker3.Properties.Settings.Default.StartMinimized) {
84 | mainWindow.ShowActivated = false;
85 | mainWindow.Minimize();
86 |
87 | if (!EZBlocker3.Properties.Settings.Default.MinimizeToTray)
88 | mainWindow.Show();
89 | } else {
90 | mainWindow.Show();
91 | }
92 | }
93 |
94 | protected override void OnExit(ExitEventArgs e) {
95 | base.OnExit(e);
96 | GlobalSingletons.Dispose();
97 | }
98 |
99 | protected override void OnSessionEnding(SessionEndingCancelEventArgs e) {
100 | // Cancel forced shutdown and shutdown normally.
101 | // This allows the cleanup code after the app.Run() method in Program.cs to run.
102 | e.Cancel = true;
103 | Shutdown();
104 |
105 | base.OnSessionEnding(e);
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/EZBlocker3/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 | )]
11 |
--------------------------------------------------------------------------------
/EZBlocker3/Audio/CoreAudio/AudioDevice.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 | using Microsoft.Windows.Sdk;
4 |
5 | namespace EZBlocker3.Audio.CoreAudio {
6 | public unsafe class AudioDevice : IDisposable {
7 | private readonly IMMDevice device;
8 |
9 | public AudioDevice(IMMDevice device) {
10 | this.device = device;
11 | }
12 |
13 | public static AudioDevice GetDefaultAudioDevice(EDataFlow dataFlow, ERole role) {
14 | IMMDeviceEnumerator? deviceEnumerator = null;
15 | try {
16 | PInvoke.CoCreateInstance(typeof(MMDeviceEnumerator).GUID, null, (uint)CLSCTX.CLSCTX_INPROC_SERVER, typeof(IMMDeviceEnumerator).GUID, out var tmp);
17 | deviceEnumerator = (IMMDeviceEnumerator)tmp;
18 | deviceEnumerator.GetDefaultAudioEndpoint(dataFlow, role, out IMMDevice device);
19 | return new AudioDevice(device);
20 | } finally {
21 | if (deviceEnumerator != null)
22 | Marshal.FinalReleaseComObject(deviceEnumerator);
23 | }
24 | }
25 |
26 | public AudioSessionManager GetSessionManager() {
27 | device.Activate(typeof(IAudioSessionManager2).GUID, 0, default, out var sessionManager);
28 | return new AudioSessionManager((IAudioSessionManager2)Marshal.GetObjectForIUnknown((IntPtr)sessionManager));
29 | }
30 |
31 | #region IDisposable
32 | private bool isDisposed;
33 | protected virtual void Dispose(bool disposing) {
34 | if (!isDisposed) {
35 | isDisposed = true;
36 |
37 | Marshal.FinalReleaseComObject(device);
38 | }
39 | }
40 |
41 | ~AudioDevice() {
42 | Dispose(disposing: false);
43 | }
44 |
45 | public void Dispose() {
46 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
47 | Dispose(disposing: true);
48 | GC.SuppressFinalize(this);
49 | }
50 | #endregion IDisposable
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/EZBlocker3/Audio/CoreAudio/AudioSession.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.ConstrainedExecution;
3 | using System.Runtime.InteropServices;
4 | using Microsoft.Windows.Sdk;
5 |
6 | namespace EZBlocker3.Audio.CoreAudio {
7 | public unsafe class AudioSession : CriticalFinalizerObject, IDisposable {
8 | private readonly IAudioSessionControl audioSessionControl;
9 | private readonly IAudioSessionControl2? audioSessionControl2;
10 | private readonly ISimpleAudioVolume? simpleAudioVolume;
11 | private readonly IAudioMeterInformation? audioMeterInformation;
12 |
13 | public AudioSession(IAudioSessionControl session) {
14 | audioSessionControl = session;
15 |
16 | simpleAudioVolume = session as ISimpleAudioVolume;
17 | audioMeterInformation = session as IAudioMeterInformation;
18 | audioSessionControl2 = session as IAudioSessionControl2;
19 | }
20 |
21 | public uint ProcessID {
22 | get {
23 | if (audioSessionControl2 is null)
24 | throw new NotSupportedException();
25 | audioSessionControl2.GetProcessId(out var processId);
26 | return processId;
27 | }
28 | }
29 |
30 | public bool IsMuted {
31 | get {
32 | if (simpleAudioVolume is null)
33 | throw new NotSupportedException();
34 | simpleAudioVolume.GetMute(out var isMuted);
35 | return isMuted;
36 | }
37 | set {
38 | if (simpleAudioVolume is null)
39 | throw new NotSupportedException();
40 | simpleAudioVolume.SetMute(value, default);
41 | }
42 | }
43 |
44 | public float MasterVolume {
45 | get {
46 | if (simpleAudioVolume is null)
47 | throw new NotSupportedException();
48 | simpleAudioVolume.GetMasterVolume(out var level);
49 | return level;
50 | }
51 | set {
52 | if (simpleAudioVolume is null)
53 | throw new NotSupportedException();
54 | simpleAudioVolume.SetMasterVolume(value, default);
55 | }
56 | }
57 |
58 | public float PeakVolume {
59 | get {
60 | if (audioMeterInformation is null)
61 | throw new NotSupportedException();
62 | audioMeterInformation.GetPeakValue(out var peak);
63 | return peak;
64 | }
65 | }
66 |
67 | #region IDisposable
68 | private bool _disposed;
69 | protected virtual void Dispose(bool disposing) {
70 | if (!_disposed) {
71 | _disposed = true;
72 |
73 | if (audioSessionControl != null)
74 | Marshal.FinalReleaseComObject(audioSessionControl);
75 | if (audioSessionControl2 != null)
76 | Marshal.FinalReleaseComObject(audioSessionControl2);
77 | if (simpleAudioVolume != null)
78 | Marshal.FinalReleaseComObject(simpleAudioVolume);
79 | if (audioMeterInformation != null)
80 | Marshal.FinalReleaseComObject(audioMeterInformation);
81 | }
82 | }
83 |
84 | ~AudioSession() {
85 | Dispose(disposing: false);
86 | }
87 |
88 | public void Dispose() {
89 | Dispose(disposing: true);
90 | GC.SuppressFinalize(this);
91 | }
92 | #endregion
93 | }
94 | }
--------------------------------------------------------------------------------
/EZBlocker3/Audio/CoreAudio/AudioSessionCollection.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.ConstrainedExecution;
3 | using System.Runtime.InteropServices;
4 | using Microsoft.Windows.Sdk;
5 |
6 | namespace EZBlocker3.Audio.CoreAudio {
7 | public unsafe class AudioSessionCollection : CriticalFinalizerObject, IDisposable {
8 | private readonly IAudioSessionEnumerator sessionEnumerator;
9 |
10 | public AudioSessionCollection(IAudioSessionEnumerator sessionEnumerator) {
11 | this.sessionEnumerator = sessionEnumerator;
12 | }
13 |
14 | public AudioSession this[int index] {
15 | get {
16 | sessionEnumerator.GetSession(index, out var session);
17 | return new AudioSession(session);
18 | }
19 | }
20 |
21 | public int Count {
22 | get {
23 | sessionEnumerator.GetCount(out var count);
24 | return count;
25 | }
26 | }
27 |
28 | #region IDisposable
29 | private bool _disposed;
30 | protected virtual void Dispose(bool disposing) {
31 | if (!_disposed) {
32 | _disposed = true;
33 |
34 | if (sessionEnumerator != null)
35 | Marshal.FinalReleaseComObject(sessionEnumerator);
36 | }
37 | }
38 |
39 | ~AudioSessionCollection() {
40 | Dispose(disposing: false);
41 | }
42 |
43 | public void Dispose() {
44 | Dispose(disposing: true);
45 | GC.SuppressFinalize(this);
46 | }
47 | #endregion
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/EZBlocker3/Audio/CoreAudio/AudioSessionManager.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.ConstrainedExecution;
3 | using System.Runtime.InteropServices;
4 | using Microsoft.Windows.Sdk;
5 |
6 | namespace EZBlocker3.Audio.CoreAudio {
7 | public unsafe class AudioSessionManager : CriticalFinalizerObject, IDisposable {
8 | private readonly IAudioSessionManager2 sessionManager;
9 |
10 | public AudioSessionManager(IAudioSessionManager2 sessionManager) {
11 | this.sessionManager = sessionManager;
12 | }
13 |
14 | public AudioSessionCollection GetSessionCollection() {
15 | IAudioSessionEnumerator? sessionEnumerator = null;
16 | try {
17 | sessionEnumerator = sessionManager.GetSessionEnumerator();
18 | return new AudioSessionCollection(sessionEnumerator);
19 | } catch {
20 | if (sessionEnumerator != null)
21 | Marshal.FinalReleaseComObject(sessionEnumerator);
22 | throw;
23 | }
24 | }
25 |
26 | #region IDisposable
27 | private bool _disposed;
28 | protected virtual void Dispose(bool disposing) {
29 | if (!_disposed) {
30 | _disposed = true;
31 |
32 | if (sessionManager != null)
33 | Marshal.FinalReleaseComObject(sessionManager);
34 | }
35 | }
36 |
37 | ~AudioSessionManager() {
38 | Dispose(disposing: false);
39 | }
40 |
41 | public void Dispose() {
42 | Dispose(disposing: true);
43 | GC.SuppressFinalize(this);
44 | }
45 | #endregion
46 | }
47 | }
--------------------------------------------------------------------------------
/EZBlocker3/Audio/VolumeMixer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 |
4 | namespace EZBlocker3.Audio {
5 | public static class VolumeMixer {
6 | public static readonly string Path = Environment.GetEnvironmentVariable("WINDIR") + @"\System32\SndVol.exe";
7 |
8 | public static void Open() => Process.Start(Path).Dispose();
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/EZBlocker3/AutoUpdate/DownloadUpdateWindow.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/EZBlocker3/AutoUpdate/DownloadUpdateWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using System.Windows;
5 | using System.Windows.Shell;
6 | using EZBlocker3.Extensions;
7 | using EZBlocker3.Logging;
8 |
9 | namespace EZBlocker3.AutoUpdate {
10 | public partial class DownloadUpdateWindow : Window {
11 | private readonly UpdateInfo Update;
12 | private readonly CancellationTokenSource _cancellationSource = new();
13 |
14 | public DownloadUpdateWindow(UpdateInfo update) {
15 | Update = update;
16 | InitializeComponent();
17 |
18 | abortDownloadButton.Click += (_, __) => {
19 | _cancellationSource.Cancel();
20 | Close();
21 | };
22 |
23 | Task.Run(DownloadUpdate, _cancellationSource.Token);
24 | }
25 |
26 | protected override void OnClosed(EventArgs e) {
27 | base.OnClosed(e);
28 |
29 | _cancellationSource.Cancel();
30 | }
31 |
32 | private async Task DownloadUpdate() {
33 | var downloader = new UpdateDownloader();
34 | downloader.Progress += (s, e) => {
35 | Dispatcher.BeginInvoke(() => {
36 | var normalizedPercentage = e.DownloadPercentage;
37 | var percentage = normalizedPercentage * 100;
38 | downloadProgress.Value = percentage;
39 | downloadState.Text = $"Downloading... {Math.Round(percentage)}%";
40 | Owner.TaskbarItemInfo = new TaskbarItemInfo() {
41 | ProgressValue = normalizedPercentage,
42 | ProgressState = TaskbarItemProgressState.Normal
43 | };
44 | });
45 | };
46 |
47 | DownloadedUpdate? downloadedUpdate = null;
48 | try {
49 | downloadedUpdate = await downloader.Download(Update, _cancellationSource.Token).ConfigureAwait(false);
50 | } catch (Exception e) {
51 | Logger.LogException("AutoUpdate: Update download failed", e);
52 | await Dispatcher.InvokeAsync(() => {
53 | MessageBox.Show("Update download failed!", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
54 | Close();
55 | }, _cancellationSource.Token);
56 | return;
57 | }
58 |
59 | await Dispatcher.InvokeAsync(() => {
60 | downloadState.Text = "Installing...";
61 | Owner.TaskbarItemInfo = new TaskbarItemInfo() {
62 | ProgressState = TaskbarItemProgressState.Indeterminate
63 | };
64 |
65 | try {
66 | UpdateInstaller.InstallUpdateAndRestart(downloadedUpdate);
67 | } catch (Exception e) {
68 | Logger.LogException("AutoUpdate: Update install failed", e);
69 | MessageBox.Show("Failed to install update!", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
70 | Close();
71 | return;
72 | }
73 |
74 | Owner.TaskbarItemInfo = new TaskbarItemInfo() {
75 | ProgressValue = 0,
76 | ProgressState = TaskbarItemProgressState.None
77 | };
78 | }, _cancellationSource.Token);
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/EZBlocker3/AutoUpdate/DownloadedUpdate.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 |
4 | namespace EZBlocker3.AutoUpdate {
5 | public class DownloadedUpdate : IDisposable {
6 | public MemoryStream UpdateBytes { get; }
7 |
8 | public DownloadedUpdate(MemoryStream updateBytes) {
9 | UpdateBytes = updateBytes;
10 | }
11 |
12 | public void Dispose() {
13 | UpdateBytes.Dispose();
14 | GC.SuppressFinalize(this);
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/EZBlocker3/AutoUpdate/UpdateChecker.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Net.Http;
5 | using System.Net.Http.Headers;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using EZBlocker3.Logging;
9 | using Newtonsoft.Json;
10 | using Newtonsoft.Json.Linq;
11 |
12 | namespace EZBlocker3.AutoUpdate {
13 | public static class UpdateChecker {
14 | private const string RELEASES_ENDPOINT = "https://api.github.com/repos/OpenByteDev/EZBlocker3/releases";
15 |
16 | private static Version GetCurrentVersion() {
17 | if (App.ForceUpdate)
18 | return new Version("0.0.0.0");
19 | else
20 | return App.Version;
21 | }
22 |
23 | public static async Task CheckForUpdate(CancellationToken cancellationToken = default) {
24 | Logger.AutoUpdate.LogDebug("Start update check");
25 |
26 | cancellationToken.ThrowIfCancellationRequested();
27 |
28 | var client = GlobalSingletons.HttpClient;
29 | var request = new HttpRequestMessage() {
30 | RequestUri = new Uri(RELEASES_ENDPOINT),
31 | Method = HttpMethod.Get
32 | };
33 | request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
34 | request.Headers.Add("User-Agent", "EZBlocker3 Auto Updater");
35 |
36 | var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
37 | if (!response.IsSuccessStatusCode) {
38 | Logger.AutoUpdate.LogError($"Update check failed (Request failed with status code {response.StatusCode})");
39 | return null;
40 | }
41 |
42 | var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
43 | using var reader = new JsonTextReader(new StreamReader(responseStream));
44 |
45 | var releases = await JToken.ReadFromAsync(reader, cancellationToken).ConfigureAwait(false);
46 | var latestReleaseInfo = releases?.FirstOrDefault(e => !e.Value("prerelease"));
47 | var latestVersionString = latestReleaseInfo?.Value("tag_name");
48 | if (latestVersionString is null) {
49 | Logger.AutoUpdate.LogError("Update check failed (Failed to detect latest version)");
50 | return null;
51 | }
52 |
53 | var latestVersion = new Version(latestVersionString);
54 | var currentVersion = GetCurrentVersion();
55 | if (latestVersion <= currentVersion) {
56 | Logger.AutoUpdate.LogInfo($"Currently running latest version. ({latestVersion})");
57 | return null;
58 | }
59 | Logger.AutoUpdate.LogInfo($"Updated detected {currentVersion} -> {latestVersion}");
60 |
61 | var downloadUrl = latestReleaseInfo?["assets"]?
62 | .Where(e => e.Value("content_type") == "application/x-msdownload")
63 | .Select(e => e.Value("browser_download_url"))
64 | .FirstOrDefault();
65 | if (downloadUrl is null) {
66 | Logger.AutoUpdate.LogError("Update check failed (Failed to find download url)");
67 | return null;
68 | }
69 | return new UpdateInfo(downloadUrl, currentVersion, latestVersion);
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/EZBlocker3/AutoUpdate/UpdateDownloader.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Net.Http;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using EZBlocker3.Extensions;
7 | using EZBlocker3.Logging;
8 |
9 | namespace EZBlocker3.AutoUpdate {
10 | public class UpdateDownloader {
11 | public event EventHandler? Progress;
12 |
13 | public async Task Download(UpdateInfo update, CancellationToken cancellationToken = default) {
14 | Logger.AutoUpdate.LogInfo("Start downloading update");
15 |
16 | cancellationToken.ThrowIfCancellationRequested();
17 |
18 | // download file
19 | var client = GlobalSingletons.HttpClient;
20 | using var response = await client.GetAsync(update.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
21 | response.EnsureSuccessStatusCode();
22 |
23 | Logger.AutoUpdate.LogDebug("Received response headers");
24 |
25 | using var contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false);
26 | var contentLength = response.Content.Headers.ContentLength;
27 |
28 | // copy to memory stream
29 | var memoryStream = new MemoryStream();
30 | if (contentLength is long totalBytes) {
31 | var progressHandler = new Progress(bytesReceived => {
32 | Progress?.Invoke(this, new DownloadProgressEventArgs(bytesReceived, totalBytes));
33 | Logger.AutoUpdate.LogDebug($"Received {bytesReceived}/{totalBytes} bytes");
34 | });
35 | await contentStream.CopyToAsync(memoryStream, progressHandler, cancellationToken).ConfigureAwait(false);
36 | } else {
37 | Logger.AutoUpdate.LogWarning("Failed to determine response content length.");
38 | await contentStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
39 | }
40 | memoryStream.Seek(0, SeekOrigin.Begin);
41 |
42 | Logger.AutoUpdate.LogInfo("Completed update download");
43 |
44 | return new DownloadedUpdate(memoryStream);
45 | }
46 | }
47 |
48 | public class DownloadProgressEventArgs : EventArgs {
49 | public float DownloadPercentage => (float)BytesReceived / TotalBytesToReceive;
50 | public long BytesReceived { get; }
51 | public long TotalBytesToReceive { get; }
52 |
53 | public DownloadProgressEventArgs(long bytesReceived, long totalBytesToReceive) {
54 | BytesReceived = bytesReceived;
55 | TotalBytesToReceive = totalBytesToReceive;
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/EZBlocker3/AutoUpdate/UpdateFoundWindow.xaml:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/EZBlocker3/AutoUpdate/UpdateFoundWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace EZBlocker3.AutoUpdate {
4 | public partial class UpdateFoundWindow : Window {
5 | public enum UpdateDecision {
6 | Accept,
7 | NotNow,
8 | IgnoreUpdate
9 | }
10 |
11 | public UpdateDecision? Decision { get; private set; }
12 |
13 | public UpdateFoundWindow(UpdateInfo update) {
14 | InitializeComponent();
15 |
16 | versionInfoLabel.Text = $"{update.CurrentVersion} -> {update.UpdateVersion}";
17 |
18 | acceptDownloadButton.Click += (_, __) => Close(UpdateDecision.Accept);
19 | notNowButton.Click += (_, __) => Close(UpdateDecision.NotNow);
20 | ignoreUpdateButton.Click += (_, __) => Close(UpdateDecision.IgnoreUpdate);
21 | }
22 |
23 | private void Close(UpdateDecision decision) {
24 | Decision = decision;
25 | Close();
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/EZBlocker3/AutoUpdate/UpdateInfo.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace EZBlocker3.AutoUpdate {
4 | public record UpdateInfo(string DownloadUrl, Version CurrentVersion, Version UpdateVersion);
5 | }
6 |
--------------------------------------------------------------------------------
/EZBlocker3/AutoUpdate/UpdateInstaller.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Threading.Tasks;
5 | using System.Windows;
6 | using EZBlocker3.Logging;
7 | using EZBlocker3.Settings;
8 | using Lazy;
9 |
10 | namespace EZBlocker3.AutoUpdate {
11 | public static class UpdateInstaller {
12 | [Lazy]
13 | private static string TempOldAppPath => Path.ChangeExtension(App.Location, ".exe.bak");
14 |
15 | [Lazy]
16 | private static string TempNewAppPath => Path.ChangeExtension(App.Location, ".exe.upd");
17 |
18 | public static void InstallUpdateAndRestart(DownloadedUpdate update) {
19 | try {
20 | Logger.AutoUpdate.LogDebug("Begin install");
21 |
22 | File.Delete(TempNewAppPath);
23 | using (var tempNewAppFile = File.OpenWrite(TempNewAppPath))
24 | update.UpdateBytes.WriteTo(tempNewAppFile);
25 |
26 | Logger.AutoUpdate.LogDebug("Extracted update");
27 |
28 | File.Delete(TempOldAppPath);
29 | File.Move(App.Location, TempOldAppPath);
30 | File.Move(TempNewAppPath, App.Location);
31 |
32 | Logger.AutoUpdate.LogDebug("Replaced executable");
33 |
34 | // disable settings with external state before upgrade, as the new version maybe uses a different system.
35 | StartWithSpotify.Disable();
36 | Autostart.Disable();
37 |
38 | Logger.AutoUpdate.LogDebug("Restarting");
39 | Process.Start(App.Location, "/updateRestart").Dispose();
40 | Application.Current.Dispatcher.Invoke(() => Application.Current.Shutdown());
41 | } catch (Exception e) {
42 | Logger.AutoUpdate.LogException("Installation failed:", e);
43 | Logger.AutoUpdate.LogInfo("Starting failure cleanup");
44 | // cleanup failed installation
45 |
46 | // try to restore app executable
47 | if (!File.Exists(App.Location)) {
48 | if (File.Exists(TempOldAppPath))
49 | File.Move(TempOldAppPath, App.Location);
50 | else if (File.Exists(TempNewAppPath))
51 | File.Move(TempNewAppPath, App.Location);
52 | }
53 |
54 | // delete update file if it still exists
55 | File.Delete(TempNewAppPath);
56 |
57 | Logger.AutoUpdate.LogInfo("Finished failure cleanup");
58 |
59 | // rethrow exception
60 | throw;
61 | } finally {
62 | update.Dispose();
63 | }
64 | }
65 |
66 | public static void CleanupUpdate() {
67 | Task.Run(async () => {
68 | await Task.Delay(10000).ConfigureAwait(false);
69 | File.Delete(TempOldAppPath);
70 | });
71 | }
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/EZBlocker3/CliArgs.cs:
--------------------------------------------------------------------------------
1 | using EZBlocker3.Logging;
2 |
3 | namespace EZBlocker3 {
4 | public record CliArgs(bool ForceDebugMode = false, bool IsUpdateRestart = false, bool IsProxyStart = false, bool IsAutomaticStart = false) {
5 |
6 | public const string ForceDebugOption = "/debug";
7 | public const string UpdateRestartOption = "/updateRestart";
8 | public const string ProxyStartOption = "/proxyStart";
9 | public const string AutomaticStartOption = "/autostart";
10 |
11 | public static CliArgs Parse(string[] args) {
12 | var forceDebugMode = false;
13 | var isUpdateRestart = false;
14 | var isProxyStart = false;
15 | var isAutomaticStart = false;
16 |
17 | foreach (var arg in args) {
18 | switch (arg) {
19 | case ForceDebugOption:
20 | forceDebugMode = true;
21 | break;
22 | case UpdateRestartOption:
23 | isUpdateRestart = true;
24 | break;
25 | case ProxyStartOption:
26 | isProxyStart = true;
27 | break;
28 | case AutomaticStartOption:
29 | isAutomaticStart = true;
30 | break;
31 | default:
32 | Logger.LogInfo("Unrecognised cli arg:" + arg);
33 | break;
34 | }
35 | }
36 |
37 | return new CliArgs() {
38 | ForceDebugMode = forceDebugMode,
39 | IsUpdateRestart = isUpdateRestart,
40 | IsProxyStart = isProxyStart,
41 | IsAutomaticStart = isAutomaticStart
42 | };
43 | }
44 |
45 | }
46 | }
--------------------------------------------------------------------------------
/EZBlocker3/EZBlocker3.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | True
5 | True
6 | Enable
7 | Icon/icon256.ico
8 | True
9 | True
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | Code
46 |
47 |
48 | True
49 | True
50 | Resources.resx
51 |
52 |
53 | True
54 | True
55 | Settings.settings
56 |
57 |
58 |
59 |
60 |
61 | ResXFileCodeGenerator
62 | Resources.Designer.cs
63 |
64 |
65 |
66 |
67 |
68 | SettingsSingleFileGenerator
69 | Settings.Designer.cs
70 |
71 |
72 |
73 |
74 |
75 | Designer
76 |
77 |
78 |
79 |
80 | WinExe
81 | x64
82 | net472
83 | 9.0
84 | win10-x64
85 | True
86 | True
87 | True
88 |
89 |
90 |
91 | True
92 | True
93 | True
94 |
95 |
96 |
97 | False
98 |
99 |
100 | True
101 |
102 |
103 |
104 | 1.2.0.0
105 | OpenByte
106 | Icon\Icon256.png
107 | en
108 | true
109 | GPL-3.0 License
110 | LICENSE
111 | https://github.com/OpenByteDev/EZBlocker3
112 | https://github.com/OpenByteDev/EZBlocker3
113 | git
114 | ad blocker;ad muter;spotify
115 | app.manifest
116 | EZBlocker 3 is a Spotify Ad Muter/Blocker for Windows written in C#. It mutes Spotify while an advertisement is playing.
117 | EZBlocker3.Program
118 |
119 |
120 |
121 |
--------------------------------------------------------------------------------
/EZBlocker3/Extensions/ArrayExtensions.cs:
--------------------------------------------------------------------------------
1 | # nullable disable
2 |
3 | namespace EZBlocker3.Extensions {
4 | internal static class ArrayExtensions {
5 | public static void Deconstruct(this T[] items, out T t0) {
6 | t0 = items.Length > 0 ? items[0] : default;
7 | }
8 |
9 | public static void Deconstruct(this T[] items, out T t0, out T t1) {
10 | t0 = items.Length > 0 ? items[0] : default;
11 | t1 = items.Length > 1 ? items[1] : default;
12 | }
13 |
14 | public static void Deconstruct(this T[] items, out T t0, out T t1, out T t2) {
15 | t0 = items.Length > 0 ? items[0] : default;
16 | t1 = items.Length > 1 ? items[1] : default;
17 | t2 = items.Length > 2 ? items[2] : default;
18 | }
19 |
20 | public static void Deconstruct(this T[] items, out T t0, out T t1, out T t2, out T t3) {
21 | t0 = items.Length > 0 ? items[0] : default;
22 | t1 = items.Length > 1 ? items[1] : default;
23 | t2 = items.Length > 2 ? items[2] : default;
24 | t3 = items.Length > 3 ? items[3] : default;
25 | }
26 |
27 | public static void Deconstruct(this T[] items, out T t0, out T t1, out T t2, out T t3, out T t4) {
28 | t0 = items.Length > 0 ? items[0] : default;
29 | t1 = items.Length > 1 ? items[1] : default;
30 | t2 = items.Length > 2 ? items[2] : default;
31 | t3 = items.Length > 3 ? items[3] : default;
32 | t4 = items.Length > 4 ? items[4] : default;
33 | }
34 |
35 | public static void Deconstruct(this T[] items, out T t0, out T t1, out T t2, out T t3, out T t4, out T t5) {
36 | t0 = items.Length > 0 ? items[0] : default;
37 | t1 = items.Length > 1 ? items[1] : default;
38 | t2 = items.Length > 2 ? items[2] : default;
39 | t3 = items.Length > 3 ? items[3] : default;
40 | t4 = items.Length > 4 ? items[4] : default;
41 | t5 = items.Length > 5 ? items[5] : default;
42 | }
43 |
44 | public static void Deconstruct(this T[] items, out T t0, out T t1, out T t2, out T t3, out T t4, out T t5, out T t6) {
45 | t0 = items.Length > 0 ? items[0] : default;
46 | t1 = items.Length > 1 ? items[1] : default;
47 | t2 = items.Length > 2 ? items[2] : default;
48 | t3 = items.Length > 3 ? items[3] : default;
49 | t4 = items.Length > 4 ? items[4] : default;
50 | t5 = items.Length > 5 ? items[5] : default;
51 | t6 = items.Length > 6 ? items[6] : default;
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/EZBlocker3/Extensions/DirectoryInfoExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 |
3 | namespace EZBlocker3.Extensions {
4 | internal static class DirectoryInfoExtensions {
5 | public static void RecursiveDelete(this DirectoryInfo directory) {
6 | if (!directory.Exists)
7 | return;
8 |
9 | foreach (var subDirectory in directory.EnumerateDirectories())
10 | RecursiveDelete(subDirectory);
11 |
12 | directory.Delete(recursive: true);
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/EZBlocker3/Extensions/DispatcherExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics.CodeAnalysis;
3 | using System.Threading;
4 | using System.Windows.Threading;
5 |
6 | namespace EZBlocker3.Extensions {
7 | internal static class DispatcherExtensions {
8 | [SuppressMessage("Naming", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "Matching the wrapped method")]
9 | public static DispatcherOperation InvokeAsync(this Dispatcher dispatcher, Action callback, CancellationToken cancellationToken) =>
10 | dispatcher.InvokeAsync(callback, DispatcherPriority.Normal, cancellationToken);
11 |
12 | [SuppressMessage("Naming", "RCS1047:Non-asynchronous method name should not end with 'Async'.", Justification = "Matching the wrapped method")]
13 | public static DispatcherOperation InvokeAsync(this Dispatcher dispatcher, Func callback, CancellationToken cancellationToken) =>
14 | dispatcher.InvokeAsync(callback, DispatcherPriority.Normal, cancellationToken);
15 |
16 | public static DispatcherOperation BeginInvoke(this Dispatcher dispatcher, Action callback) =>
17 | dispatcher.BeginInvoke(DispatcherPriority.Normal, callback);
18 |
19 | public static void BeginInvokeShutdown(this Dispatcher dispatcher) =>
20 | dispatcher.BeginInvokeShutdown(DispatcherPriority.Normal);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/EZBlocker3/Extensions/IEnumerableExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace EZBlocker3.Extensions {
5 | internal static class IEnumerableExtensions {
6 | public static void DisposeAll(this IEnumerable enumerable) where T : IDisposable {
7 | foreach (var item in enumerable)
8 | item?.Dispose();
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/EZBlocker3/Extensions/KeyValuePairExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace EZBlocker3.Extensions {
4 | internal static class KeyValuePairExtensions {
5 | public static void Deconstruct(this KeyValuePair keyValuePair, out TKey key, out TValue value) {
6 | key = keyValuePair.Key;
7 | value = keyValuePair.Value;
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/EZBlocker3/Extensions/PointExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace EZBlocker3.Extensions {
4 | internal static class PointExtensions {
5 | public static void Deconstruct(this Point point, out double x, out double y) =>
6 | (x, y) = (point.X, point.Y);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/EZBlocker3/Extensions/ProcessExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Linq.Expressions;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Lazy;
7 |
8 | namespace EZBlocker3.Extensions {
9 | internal static class ProcessExtensions {
10 | [Lazy]
11 | private static Func GetProcessAssociatedFunc {
12 | get {
13 | // Expression Trees let us change a private field and are faster than reflection (if called multiple times)
14 | var processParamter = Expression.Parameter(typeof(Process), "process");
15 | var associatedProperty = Expression.Property(processParamter, "Associated");
16 | var lambda = Expression.Lambda>(associatedProperty, processParamter);
17 | return lambda.Compile();
18 | }
19 | }
20 | public static bool IsAssociated(this Process process) {
21 | return GetProcessAssociatedFunc(process);
22 | }
23 |
24 | public static async Task WaitForExitAsync(this Process process, CancellationToken cancellationToken = default) {
25 | var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
26 |
27 | try {
28 | if (process.HasExited)
29 | return;
30 |
31 | process.EnableRaisingEvents = true;
32 | process.Exited += Process_Exited;
33 |
34 | using var registration = cancellationToken.Register(() => tcs.TrySetCanceled(), false);
35 | await tcs.Task.ConfigureAwait(false);
36 | } finally {
37 | process.Exited -= Process_Exited;
38 | }
39 |
40 | void Process_Exited(object sender, EventArgs e) {
41 | tcs.TrySetResult(true);
42 | }
43 | }
44 |
45 | public static Task WaitForExitAsync(this Process process, TimeSpan timeout, CancellationToken cancellationToken = default) {
46 | return Task.WhenAny(process.WaitForExitAsync(cancellationToken), Task.Delay(timeout, cancellationToken)).Unwrap();
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/EZBlocker3/Extensions/QueueExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 |
3 | namespace EZBlocker3.Extensions {
4 | internal static class QueueExtensions {
5 | public static bool TryDequeue(this Queue queue, /*[NotNullWhen(true)]*/ out T? result) {
6 | if (queue.Count == 0) {
7 | result = default!;
8 | return false;
9 | } else {
10 | result = queue.Dequeue();
11 | return true;
12 | }
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/EZBlocker3/Extensions/SizeExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | namespace EZBlocker3.Extensions {
4 | internal static class SizeExtensions {
5 | public static void Deconstruct(this Size size, out double width, out double height) =>
6 | (width, height) = (size.Width, size.Height);
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/EZBlocker3/Extensions/StreamExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace EZBlocker3.Extensions {
7 | internal static class StreamExtensions {
8 | // https://referencesource.microsoft.com/#mscorlib/system/io/stream.cs,2a0f078c2e0c0aa8
9 | private const int DefaultCopyBufferSize = 81920;
10 |
11 | public static Task CopyToAsync(this Stream source, Stream destination, CancellationToken cancellationToken) =>
12 | source.CopyToAsync(destination, DefaultCopyBufferSize, cancellationToken);
13 | public static Task CopyToAsync(this Stream source, Stream destination, IProgress? progress = null, CancellationToken cancellationToken = default) =>
14 | CopyToAsync(source, destination, DefaultCopyBufferSize, progress, cancellationToken);
15 | public static Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress? progress = null, CancellationToken cancellationToken = default) {
16 | if (source == null)
17 | throw new ArgumentNullException(nameof(source));
18 | if (progress == null)
19 | return source.CopyToAsync(destination, bufferSize, cancellationToken);
20 |
21 | // from https://referencesource.microsoft.com/#mscorlib/system/io/unmanagedmemorystreamwrapper.cs,05bf6506f3abc6ed
22 | if (destination == null)
23 | throw new ArgumentNullException(nameof(destination));
24 | if (bufferSize <= 0)
25 | throw new ArgumentOutOfRangeException(nameof(bufferSize), "Positive number required."); // Environment.GetResourceString("ArgumentOutOfRange_NeedPosNum")
26 | if (!source.CanRead && !source.CanWrite)
27 | throw new ObjectDisposedException(null, "Cannot access a closed stream."); // Environment.GetResourceString("ObjectDisposed_StreamClosed")
28 | if (!destination.CanRead && !destination.CanWrite)
29 | throw new ObjectDisposedException(nameof(destination), "Cannot access a closed stream."); // Environment.GetResourceString("ObjectDisposed_StreamClosed")
30 | if (!source.CanRead)
31 | throw new NotSupportedException("Stream does not support reading."); // Environment.GetResourceString("NotSupported_UnreadableStream")
32 | if (!destination.CanWrite)
33 | throw new NotSupportedException("Stream does not support writing."); // Environment.GetResourceString("NotSupported_UnwritableStream");
34 |
35 | return Core();
36 |
37 | async Task Core() {
38 | var buffer = new byte[bufferSize];
39 | long totalBytesRead = 0;
40 | int bytesRead;
41 | while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) {
42 | await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
43 | totalBytesRead += bytesRead;
44 | progress.Report(totalBytesRead);
45 | }
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/EZBlocker3/Extensions/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace EZBlocker3.Extensions {
4 | internal static class StringExtensions {
5 | public static string[] Split(this string str, string delimiter) =>
6 | str.Split(delimiter, StringSplitOptions.None);
7 | public static string[] Split(this string str, string delimiter, int maxCount) =>
8 | str.Split(delimiter, maxCount, StringSplitOptions.None);
9 | public static string[] Split(this string str, string delimiter, StringSplitOptions options) =>
10 | str.Split(delimiter, int.MaxValue, options);
11 | public static string[] Split(this string str, string delimiter, int maxCount, StringSplitOptions options) =>
12 | str.Split(new string[] { delimiter }, maxCount, options);
13 |
14 | public static bool Contains(this string str, string substring, StringComparison comparisonType) => str.IndexOf(substring, comparisonType) >= 0;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/EZBlocker3/Extensions/TypeExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Reflection;
3 |
4 | namespace EZBlocker3.Extensions {
5 | internal static class TypeExtensions {
6 | public static ConstructorInfo? GetConstructor(this Type type, BindingFlags bindingFlags, Type[] types) {
7 | return type.GetConstructor(bindingFlags, null, types, null);
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/EZBlocker3/FodyWeavers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/EZBlocker3/GlobalSingletons.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http;
2 |
3 | namespace EZBlocker3 {
4 | public static class GlobalSingletons {
5 | private static HttpClient? _httpClient;
6 |
7 | public static HttpClient HttpClient => _httpClient ??= new();
8 |
9 | public static void Dispose() {
10 | _httpClient?.Dispose();
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/EZBlocker3/Icon/Icon.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenByteDev/EZBlocker3/3e868aaa54b154329cba25f46eaf18ac9eb1ffcb/EZBlocker3/Icon/Icon.ai
--------------------------------------------------------------------------------
/EZBlocker3/Icon/Icon128.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenByteDev/EZBlocker3/3e868aaa54b154329cba25f46eaf18ac9eb1ffcb/EZBlocker3/Icon/Icon128.ico
--------------------------------------------------------------------------------
/EZBlocker3/Icon/Icon128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenByteDev/EZBlocker3/3e868aaa54b154329cba25f46eaf18ac9eb1ffcb/EZBlocker3/Icon/Icon128.png
--------------------------------------------------------------------------------
/EZBlocker3/Icon/Icon16.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenByteDev/EZBlocker3/3e868aaa54b154329cba25f46eaf18ac9eb1ffcb/EZBlocker3/Icon/Icon16.ico
--------------------------------------------------------------------------------
/EZBlocker3/Icon/Icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenByteDev/EZBlocker3/3e868aaa54b154329cba25f46eaf18ac9eb1ffcb/EZBlocker3/Icon/Icon16.png
--------------------------------------------------------------------------------
/EZBlocker3/Icon/Icon256.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenByteDev/EZBlocker3/3e868aaa54b154329cba25f46eaf18ac9eb1ffcb/EZBlocker3/Icon/Icon256.ico
--------------------------------------------------------------------------------
/EZBlocker3/Icon/Icon256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenByteDev/EZBlocker3/3e868aaa54b154329cba25f46eaf18ac9eb1ffcb/EZBlocker3/Icon/Icon256.png
--------------------------------------------------------------------------------
/EZBlocker3/Icon/Icon32.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenByteDev/EZBlocker3/3e868aaa54b154329cba25f46eaf18ac9eb1ffcb/EZBlocker3/Icon/Icon32.ico
--------------------------------------------------------------------------------
/EZBlocker3/Icon/Icon32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenByteDev/EZBlocker3/3e868aaa54b154329cba25f46eaf18ac9eb1ffcb/EZBlocker3/Icon/Icon32.png
--------------------------------------------------------------------------------
/EZBlocker3/Icon/Icon64.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenByteDev/EZBlocker3/3e868aaa54b154329cba25f46eaf18ac9eb1ffcb/EZBlocker3/Icon/Icon64.ico
--------------------------------------------------------------------------------
/EZBlocker3/Icon/Icon64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenByteDev/EZBlocker3/3e868aaa54b154329cba25f46eaf18ac9eb1ffcb/EZBlocker3/Icon/Icon64.png
--------------------------------------------------------------------------------
/EZBlocker3/IllegalStateException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace EZBlocker3 {
4 | [Serializable]
5 | internal sealed class IllegalStateException : Exception {
6 | public IllegalStateException() : this("The current state of the object is illegal.") { }
7 | public IllegalStateException(string message) : base(message) { }
8 | public IllegalStateException(string message, Exception innerException) : base(message, innerException) { }
9 | // protected IllegalStateException(SerializationInfo info, StreamingContext context) : base(info, context) { }
10 | }
11 | }
--------------------------------------------------------------------------------
/EZBlocker3/ImageDictionary.xaml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/EZBlocker3/Interop/NativeUtils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Diagnostics;
5 | using System.Runtime.InteropServices;
6 | using Microsoft.Windows.Sdk;
7 |
8 | namespace EZBlocker3.Interop {
9 | internal static class NativeUtils {
10 | public static string GetWindowTitle(IntPtr handle) {
11 | return TryGetWindowTitle(handle) ?? throw new Win32Exception();
12 | }
13 | public static unsafe string? TryGetWindowTitle(IntPtr handle) {
14 | var titleLength = PInvoke.GetWindowTextLength((HWND)handle);
15 | if (titleLength == 0)
16 | return string.Empty;
17 |
18 | fixed (char* ptr = stackalloc char[titleLength]) {
19 | var title = new PWSTR(ptr);
20 | if (PInvoke.GetWindowText((HWND)handle, title, titleLength + 1) == 0)
21 | return null;
22 | return title.ToString();
23 | }
24 | }
25 |
26 | public static bool IsPopupWindow(IntPtr handle) {
27 | var style = PInvokeExtra.GetWindowLongPtr(handle, GetWindowLongPtr_nIndex.GWL_STYLE);
28 | return (style & WINDOWS_STYLE.WS_POPUP) != 0;
29 | }
30 |
31 | public static bool IsRootWindow(IntPtr handle) {
32 | return PInvoke.GetWindow((HWND)handle, GetWindow_uCmdFlags.GW_OWNER).Value == 0;
33 | }
34 |
35 | public static uint GetWindowThreadProcessId(IntPtr windowHandle) {
36 | uint processId;
37 | unsafe {
38 | Marshal.ThrowExceptionForHR((int)PInvoke.GetWindowThreadProcessId((HWND)windowHandle, &processId));
39 | }
40 | return processId;
41 | }
42 |
43 | public static List GetAllWindowsOfProcess(Process process) {
44 | var handles = new List(0);
45 |
46 | var callback = new WNDENUMPROC((hWnd, _) => { handles.Add(hWnd); return true; });
47 | foreach (ProcessThread thread in process.Threads) {
48 | PInvoke.EnumThreadWindows((uint)thread.Id, callback, default);
49 | thread.Dispose();
50 | }
51 |
52 | GC.KeepAlive(handles);
53 | GC.KeepAlive(callback);
54 |
55 | return handles;
56 | }
57 |
58 | // Process.MainWindowHandle does not consider hidden windows which is why the app failed to detect spotify in the system tray.
59 | public static IntPtr GetMainWindowOfProcess(Process process) => GetMainWindowOfProcess((uint)process.Id);
60 | public static IntPtr GetMainWindowOfProcess(uint targetProcessId) {
61 | var mainWindowHandle = IntPtr.Zero;
62 | var enumerationStopped = false;
63 |
64 | var callback = new WNDENUMPROC(EnumWindowsCallback);
65 | if (!PInvoke.EnumWindows(callback, default) && !enumerationStopped)
66 | throw new Win32Exception();
67 |
68 | GC.KeepAlive(callback);
69 |
70 | return mainWindowHandle;
71 |
72 | BOOL EnumWindowsCallback(HWND handle, LPARAM _) {
73 | var processId = GetWindowThreadProcessId(handle);
74 |
75 | // belongs to correct process?
76 | if (processId != targetProcessId)
77 | return true;
78 |
79 | // is root window?
80 | if (!IsRootWindow(handle))
81 | return true;
82 |
83 | // has window title?
84 | if (PInvoke.GetWindowTextLength(handle) == 0)
85 | return true;
86 |
87 | mainWindowHandle = handle;
88 | enumerationStopped = true;
89 | return false;
90 | }
91 | }
92 |
93 | public static string? GetMainWindowTitle(Process process) {
94 | var mainWindowHandle = GetMainWindowOfProcess(process);
95 |
96 | if (mainWindowHandle == IntPtr.Zero)
97 | return null;
98 |
99 | return GetWindowTitle(mainWindowHandle);
100 | }
101 |
102 | public static unsafe string GetWindowClassName(IntPtr windowHandle) {
103 | fixed (char* ptr = stackalloc char[256]) { // 256 is the max name length
104 | var name = new PWSTR(ptr);
105 | var actualNameLength = PInvoke.GetClassName((HWND)windowHandle, name, 256);
106 | if (actualNameLength == 0)
107 | throw new Win32Exception();
108 |
109 | return name.ToString();
110 | }
111 | }
112 |
113 | public static void CloseWindow(IntPtr windowHandle) {
114 | if (!PInvoke.CloseWindow((HWND)windowHandle))
115 | throw new Win32Exception();
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/EZBlocker3/Interop/PInvokeExtra.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Runtime.InteropServices;
3 | using Microsoft.Windows.Sdk;
4 |
5 | namespace EZBlocker3.Interop {
6 | internal static partial class PInvokeExtra {
7 | public static WINDOWS_STYLE GetWindowLongPtr(IntPtr hWnd, GetWindowLongPtr_nIndex nIndex) {
8 | return (WINDOWS_STYLE) GetWindowLongPtr(hWnd, (int)nIndex);
9 | }
10 |
11 | ///
12 | /// Retrieves information about the specified window.
13 | /// The function also retrieves the value at a specified offset into the extra window memory.
14 | ///
15 | /// A handle to the window and, indirectly, the class to which the window belongs.
16 | /// The zero-based offset to the value to be retrieved.
17 | ///
18 | /// If the function succeeds, the return value is the requested value.
19 | /// If the function fails, the return value is zero.
20 | ///
21 | [DllImport("user32.dll")]
22 | private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/EZBlocker3/IsExternalInit.cs:
--------------------------------------------------------------------------------
1 | #if !NET5_0
2 | // https://stackoverflow.com/a/62656145/6304917
3 | namespace System.Runtime.CompilerServices {
4 | internal sealed class IsExternalInit { }
5 | }
6 | #endif
7 |
--------------------------------------------------------------------------------
/EZBlocker3/Logging/LogLevel.cs:
--------------------------------------------------------------------------------
1 | namespace EZBlocker3.Logging {
2 | internal enum LogLevel {
3 | Debug,
4 | Information,
5 | Warning,
6 | Error
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/EZBlocker3/Logging/Logger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Diagnostics;
4 | using System.IO;
5 | using Lazy;
6 |
7 | namespace EZBlocker3.Logging {
8 | internal static class Logger {
9 | [Lazy]
10 | private static string LogFilePath => Path.Combine(App.Directory, "log.txt");
11 |
12 | private static readonly object _lock_logFile = new();
13 |
14 | public static void Log(LogLevel level, string message, string? area = null) {
15 | if (App.DebugModeEnabled) {
16 | if (area is null) {
17 | area = string.Empty;
18 | } else {
19 | area = $" ({area})";
20 | }
21 |
22 | var paddedLevel = level.ToString().PadRight(nameof(LogLevel.Information).Length);
23 |
24 | var formatted = $"[{DateTime.Now}] {paddedLevel}{area} {message}";
25 | Trace.WriteLine(formatted);
26 |
27 | lock (_lock_logFile) {
28 | using var writer = new StreamWriter(LogFilePath, append: true);
29 | writer.WriteLine(formatted);
30 | }
31 | }
32 | }
33 | public static void Log(LogLevel level, object message, string? area = null) => Log(level, message.ToString(), area);
34 | public static void LogDebug(object message, string? area = null) => Log(LogLevel.Debug, message, area);
35 | public static void LogInfo(object message, string? area = null) => Log(LogLevel.Information, message, area);
36 | public static void LogWarning(object message, string? area = null) => Log(LogLevel.Warning, message, area);
37 | public static void LogError(object message, string? area = null) => Log(LogLevel.Error, message, area);
38 | public static void LogLastWin32Error(string? area = null) => LogError(new Win32Exception().Message, area);
39 | public static void LogException(string message, Exception exception, string? area = null) => LogError(message + "\n" + exception.ToString(), area);
40 |
41 | public static NamedLogger GetNamed(string name) => new(name);
42 |
43 | public static NamedLogger AutoUpdate = GetNamed("AutoUpdate");
44 | public static NamedLogger Hook = GetNamed("SpotifyHook");
45 | public static NamedLogger AdSkipper = GetNamed("Ad Skipper");
46 | public static NamedLogger Proxy = GetNamed("Proxy");
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/EZBlocker3/Logging/NamedLogger.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace EZBlocker3.Logging {
4 | internal sealed class NamedLogger {
5 | private readonly string name;
6 |
7 | public NamedLogger(string name) {
8 | this.name = name;
9 | }
10 |
11 | public void Log(LogLevel level, string message) => Logger.Log(level, message, name);
12 | public void Log(LogLevel level, object message) => Logger.Log(level, message, name);
13 | public void LogDebug(object message) => Logger.LogDebug(message, name);
14 | public void LogInfo(object message) => Logger.LogInfo(message, name);
15 | public void LogWarning(object message) => Logger.LogWarning(message, name);
16 | public void LogError(object message) => Logger.LogError(message, name);
17 | public void LogLastWin32Error() => Logger.LogLastWin32Error(name);
18 | public void LogException(string message, Exception exception) => Logger.LogException(message, exception, name);
19 | }
20 | }
--------------------------------------------------------------------------------
/EZBlocker3/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/EZBlocker3/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 | using System.Diagnostics;
4 | using System.Drawing;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using System.Windows;
8 | using System.Windows.Forms;
9 | using System.Windows.Input;
10 | using System.Windows.Interop;
11 | using System.Windows.Media;
12 | using EZBlocker3.Audio;
13 | using EZBlocker3.AutoUpdate;
14 | using EZBlocker3.Extensions;
15 | using EZBlocker3.Logging;
16 | using EZBlocker3.Settings;
17 | using EZBlocker3.Spotify;
18 | using Microsoft.Windows.Sdk;
19 | using static EZBlocker3.AutoUpdate.UpdateFoundWindow;
20 | using Application = System.Windows.Application;
21 | using Brush = System.Windows.Media.Brush;
22 | using ContextMenu = System.Windows.Controls.ContextMenu;
23 | using MenuItem = System.Windows.Controls.MenuItem;
24 | using Point = System.Windows.Point;
25 | using Size = System.Windows.Size;
26 |
27 | namespace EZBlocker3 {
28 | public partial class MainWindow : Window {
29 | private SpotifyHandler spotifyHandler;
30 | private NotifyIcon notifyIcon;
31 | private readonly CancellationTokenSource cancellationSource = new();
32 |
33 | #pragma warning disable CS8618
34 | public MainWindow() {
35 | #pragma warning restore CS8618
36 | InitializeComponent();
37 |
38 | // add version to window title
39 | var version = App.Version;
40 | Title += $" v{version}";
41 |
42 | // recolor window in debug mode
43 | Properties.Settings.Default.PropertyChanged += (s, e) => {
44 | if (e.PropertyName != nameof(Properties.Settings.DebugMode))
45 | return;
46 | UpdateBorderBrush();
47 | };
48 | UpdateBorderBrush();
49 |
50 | SetupSpotifyHook();
51 | SetupNotifyIcon();
52 |
53 | OpenVolumeControlButton.Click += OpenVolumeControlButton_Click;
54 | SettingsIcon.MouseUp += SettingsIcon_MouseUp;
55 |
56 | UpdateStatusLabel();
57 |
58 | Loaded += MainWindow_Loaded;
59 |
60 | // if the screen configuration did not change, we restore the window position
61 | if (new Size(SystemParameters.VirtualScreenWidth, SystemParameters.VirtualScreenHeight) == Properties.Settings.Default.VirtualScreenSize
62 | && Properties.Settings.Default.MainWindowPosition is Point position && position.X >= 0 && position.Y >= 0) {
63 | (Left, Top) = position;
64 | }
65 |
66 | MaybePerformUpdateCheck();
67 | }
68 |
69 | #region WindowProc
70 | private void MainWindow_Loaded(object sender, RoutedEventArgs e) {
71 | var source = (HwndSource)PresentationSource.FromDependencyObject(this);
72 | source.AddHook(WindowProc);
73 | }
74 | private IntPtr WindowProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) {
75 | switch (msg) {
76 | case (int)Constants.WM_SYSCOMMAND:
77 | if ((int)wParam == Constants.SC_CLOSE) {
78 | // we manually close here, so that the "close window" command in the taskbar keeps working even if there is a dialog open.
79 | Close();
80 | }
81 | break;
82 | }
83 | return IntPtr.Zero;
84 | }
85 | #endregion
86 |
87 | #region AutoUpdate
88 | // is there a better name for this?
89 | private void MaybePerformUpdateCheck() {
90 | if (App.ForceUpdateCheck) {
91 | Task.Run(RunUpdateCheck, cancellationSource.Token);
92 | return;
93 | }
94 |
95 | if (!Properties.Settings.Default.CheckForUpdates)
96 | return;
97 |
98 | var now = DateTime.Now;
99 | if (Properties.Settings.Default.LastUpdateCheck is DateTime lastUpdateCheckDate) {
100 | var timeSinceLastUpdateCheck = now - lastUpdateCheckDate;
101 | if (timeSinceLastUpdateCheck < TimeSpan.FromDays(1))
102 | return;
103 | }
104 | Properties.Settings.Default.LastUpdateCheck = now;
105 |
106 | Task.Run(RunUpdateCheck, cancellationSource.Token);
107 | }
108 | private async Task RunUpdateCheck() {
109 | try {
110 | var result = await UpdateChecker.CheckForUpdate(cancellationSource.Token).ConfigureAwait(false);
111 | if (result is not UpdateInfo update) // No update found
112 | return;
113 |
114 | if (Properties.Settings.Default.IgnoreUpdate == update.UpdateVersion.ToString())
115 | return;
116 |
117 | await Dispatcher.InvokeAsync(() => {
118 | switch (ShowUpdateFoundWindow(update)) {
119 | case UpdateDecision.Accept:
120 | Logger.AutoUpdate.LogInfo($"Accepted update to {update.UpdateVersion}");
121 | ShowDownloadWindow(update);
122 | break;
123 | case UpdateDecision.NotNow:
124 | Logger.AutoUpdate.LogInfo($"Delayed update to {update.UpdateVersion}");
125 | break;
126 | case UpdateDecision.IgnoreUpdate:
127 | Logger.AutoUpdate.LogInfo($"Ignored update to {update.UpdateVersion}");
128 | Properties.Settings.Default.IgnoreUpdate = update.UpdateVersion.ToString();
129 | break;
130 | }
131 | }, cancellationSource.Token);
132 | } catch (Exception e) {
133 | Logger.AutoUpdate.LogException("Update operation failed", e);
134 | }
135 | }
136 |
137 | private UpdateDecision? ShowUpdateFoundWindow(UpdateInfo update) {
138 | var updateWindow = new UpdateFoundWindow(update) {
139 | Owner = this
140 | };
141 | updateWindow.ShowDialog();
142 | return updateWindow.Decision;
143 | }
144 |
145 | private bool? ShowDownloadWindow(UpdateInfo update) {
146 | var downloadUpdateWindow = new DownloadUpdateWindow(update) {
147 | Owner = this
148 | };
149 | return downloadUpdateWindow.ShowDialog();
150 | }
151 | #endregion
152 |
153 | #region Settings
154 | private void SettingsIcon_MouseUp(object sender, MouseButtonEventArgs e) {
155 | ShowSettingsWindow();
156 | }
157 |
158 | private bool? ShowSettingsWindow() {
159 | var settingsWindow = new SettingsWindow {
160 | Owner = this
161 | };
162 | return settingsWindow.ShowDialog();
163 | }
164 |
165 | #endregion
166 |
167 | #region NotifyIcon
168 | private void SetupNotifyIcon() {
169 | notifyIcon = new NotifyIcon();
170 |
171 | var sri = Application.GetResourceStream(new Uri("/Icon/Icon32.ico", UriKind.Relative));
172 | if (sri != null)
173 | notifyIcon.Icon = new Icon(sri.Stream);
174 | notifyIcon.Visible = true;
175 | notifyIcon.MouseClick += (_, e) => {
176 | switch (e.Button) {
177 | case MouseButtons.Left:
178 | Deminimize();
179 | break;
180 | case MouseButtons.Right:
181 | ShowNotifyIconContextMenu();
182 | break;
183 | }
184 | };
185 | notifyIcon.BalloonTipClicked += (_, __) => Deminimize();
186 | StateChanged += (_, __) => {
187 | if (WindowState == WindowState.Minimized && !ShowInTaskbar)
188 | notifyIcon.ShowBalloonTip(5000, "EZBlocker3", "EZBlocker3 is hidden. Click this icon to restore.", ToolTipIcon.None);
189 | };
190 | // ensure context menu gets closed.
191 | Closed += (_, __) => {
192 | CloseNotifyIconContextMenu();
193 | if (_notifyIconContextMenu != null)
194 | _notifyIconContextMenu.Visibility = Visibility.Hidden;
195 | };
196 | }
197 |
198 | private ContextMenu? _notifyIconContextMenu;
199 | private void ShowNotifyIconContextMenu() {
200 | if (_notifyIconContextMenu is null) {
201 | _notifyIconContextMenu = new ContextMenu();
202 |
203 | var showWindowItem = new MenuItem {
204 | Header = "Show Application Window"
205 | };
206 | showWindowItem.Click += (_, __) => Deminimize();
207 | _notifyIconContextMenu.Items.Add(showWindowItem);
208 |
209 | var openProjectPageItem = new MenuItem {
210 | Header = "Open Project Page"
211 | };
212 | openProjectPageItem.Click += (_, __) => {
213 | Process.Start(new ProcessStartInfo() {
214 | FileName = Properties.Resources.ProjectPageUrl,
215 | UseShellExecute = true
216 | });
217 | };
218 | _notifyIconContextMenu.Items.Add(openProjectPageItem);
219 |
220 | var exitItem = new MenuItem {
221 | Header = "Exit"
222 | };
223 | exitItem.Click += (_, __) => Application.Current.Shutdown();
224 |
225 | _notifyIconContextMenu.Items.Add(exitItem);
226 | }
227 | _notifyIconContextMenu.IsOpen = true;
228 | }
229 | private void CloseNotifyIconContextMenu() {
230 | if (_notifyIconContextMenu is null)
231 | return;
232 | _notifyIconContextMenu.IsOpen = false;
233 | }
234 | #endregion
235 |
236 | #region SpotifyHook
237 | private void SetupSpotifyHook() {
238 | spotifyHandler = new();
239 |
240 | spotifyHandler.Hook.SpotifyStateChanged += (_, __) => UpdateStatusLabel();
241 | spotifyHandler.Hook.ActiveSongChanged += (_, __) => UpdateStatusLabel();
242 | spotifyHandler.Hook.HookChanged += (_, __) => UpdateStatusLabel();
243 |
244 | spotifyHandler.Activate();
245 | }
246 |
247 | private void UpdateStatusLabel() {
248 | Dispatcher.BeginInvoke(() => StateLabel.Text = GetStateText());
249 | }
250 |
251 | private string GetStateText() {
252 | if (!spotifyHandler.Hook.IsHooked) {
253 | return "Spotify is not running.";
254 | } else {
255 | switch (spotifyHandler.Hook.State) {
256 | case SpotifyState.Paused:
257 | return "Spotify is paused.";
258 | case SpotifyState.PlayingSong:
259 | if (spotifyHandler.Hook.ActiveSong is not SongInfo song) {
260 | Logger.Hook.LogError("Active song is undefined in PlayingSong state.");
261 | throw new IllegalStateException();
262 | }
263 | return $"Playing {song.Title} by {song.Artist}";
264 | case SpotifyState.PlayingAdvertisement:
265 | return "Playing advertisement...";
266 | case SpotifyState.StartingUp:
267 | return "Spotify is starting...";
268 | case SpotifyState.ShuttingDown:
269 | return "Spotify is shutting down...";
270 | // case SpotifyState.Unknown:
271 | default:
272 | return "Spotify is an unknown state.";
273 | }
274 | }
275 | }
276 | #endregion
277 |
278 | private void OpenVolumeControlButton_Click(object sender, RoutedEventArgs e) {
279 | Task.Run(() => VolumeMixer.Open(), cancellationSource.Token);
280 | }
281 |
282 | public void Minimize() {
283 | WindowState = WindowState.Minimized;
284 |
285 | OnStateChanged(EventArgs.Empty);
286 | }
287 | public void Deminimize() {
288 | WindowState = WindowState.Normal;
289 | Show();
290 | Activate();
291 |
292 | OnStateChanged(EventArgs.Empty);
293 | }
294 |
295 | private (Brush, Thickness)? _defaultBorder;
296 | private static readonly (Brush, Thickness) _debugModeBorder = (new SolidColorBrush(Colors.OrangeRed), new Thickness(2));
297 | private void UpdateBorderBrush() {
298 | (BorderBrush, BorderThickness) = App.DebugModeEnabled ? _debugModeBorder : (_defaultBorder ??= (BorderBrush, BorderThickness));
299 | }
300 |
301 | protected override void OnStateChanged(EventArgs e) {
302 | if (Properties.Settings.Default.MinimizeToTray)
303 | ShowInTaskbar = WindowState != WindowState.Minimized;
304 |
305 | base.OnStateChanged(e);
306 | }
307 |
308 | protected override void OnClosing(CancelEventArgs e) {
309 | base.OnClosing(e);
310 |
311 | if (WindowState == WindowState.Normal) {
312 | if (!App.SaveSettingsOnClose)
313 | return;
314 |
315 | if (Left >= 0 && Top >= 0) {
316 | Properties.Settings.Default.MainWindowPosition = new Point(Left, Top);
317 | Properties.Settings.Default.VirtualScreenSize = new Size(SystemParameters.VirtualScreenWidth, SystemParameters.VirtualScreenHeight);
318 | }
319 | Properties.Settings.Default.Save();
320 | }
321 | }
322 |
323 | protected override void OnClosed(EventArgs e) {
324 | base.OnClosed(e);
325 |
326 | cancellationSource.Cancel();
327 |
328 | if (Properties.Settings.Default.UnmuteOnClose)
329 | spotifyHandler.Muter?.Unmute();
330 |
331 | if (notifyIcon != null) {
332 | notifyIcon.Visible = false;
333 | notifyIcon.Icon?.Dispose();
334 | notifyIcon.Dispose();
335 | }
336 |
337 | spotifyHandler.Deactivate();
338 | spotifyHandler.Dispose();
339 | }
340 | }
341 | }
342 |
--------------------------------------------------------------------------------
/EZBlocker3/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.IO.Pipes;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using System.Windows;
7 | using EZBlocker3.AutoUpdate;
8 | using EZBlocker3.Extensions;
9 | using EZBlocker3.Logging;
10 | using EZBlocker3.Settings;
11 |
12 | namespace EZBlocker3 {
13 | internal static class Program {
14 | private static readonly string SingletonMutexName = App.Name + "_SingletonMutex";
15 | private static readonly string PipeName = App.Name + "_IPC";
16 |
17 | #pragma warning disable CS8618
18 | public static CliArgs CliArgs;
19 | #pragma warning restore CS8618
20 |
21 | [STAThread]
22 | public static int Main(string[] args) {
23 | AppDomain.CurrentDomain.UnhandledException += (s, e) => OnUnhandledException(e.ExceptionObject as Exception);
24 |
25 | CliArgs = CliArgs.Parse(args);
26 |
27 | if (args.Length == 0) {
28 | Logger.LogInfo("Started without cli args");
29 | } else {
30 | Logger.LogInfo("Started with args: " + string.Join(" ", args));
31 | }
32 |
33 | using var mutex = new Mutex(initiallyOwned: true, SingletonMutexName, out var notAlreadyRunning);
34 |
35 | if (CliArgs.IsUpdateRestart) {
36 | try {
37 | // wait for old version to exit and release the mutex.
38 | mutex.WaitOne(TimeSpan.FromSeconds(10), exitContext: false);
39 | UpdateInstaller.CleanupUpdate();
40 | } catch (Exception e) {
41 | Logger.LogException("Restart failed after update", e);
42 | }
43 |
44 | // the application has exited so we are not already running.
45 | notAlreadyRunning = true;
46 | }
47 |
48 | if (notAlreadyRunning) { // we are the only one around :(
49 | var cancellationTokenSource = new CancellationTokenSource();
50 | Task.Run(() => RunPipeServer(cancellationTokenSource.Token), cancellationTokenSource.Token);
51 |
52 | var exitCode = RunApp();
53 | Logger.LogInfo($"App exited with code {exitCode}");
54 |
55 | cancellationTokenSource.Cancel();
56 | mutex.ReleaseMutex();
57 | return exitCode;
58 | } else { // another instance is already running
59 | using var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out, PipeOptions.Asynchronous);
60 | client.Connect(TimeSpan.FromSeconds(10).Milliseconds);
61 |
62 | if (CliArgs.IsProxyStart) {
63 | using var writer = new StreamWriter(client);
64 | writer.WriteLine(CliArgs.ProxyStartOption);
65 | writer.Flush();
66 | }
67 |
68 | return 0;
69 | }
70 | }
71 |
72 | private static void OnUnhandledException(Exception? exception) {
73 | try {
74 | if (exception is not null) {
75 | Logger.LogException("Unhandled exception", exception);
76 | } else {
77 | Logger.LogError("Unhandled unknown exception");
78 | }
79 | } catch { }
80 | }
81 |
82 | private static async Task RunPipeServer(CancellationToken cancellationToken) {
83 | using var server = new NamedPipeServerStream(PipeName, PipeDirection.In, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
84 |
85 | cancellationToken.Register(() => {
86 | if (server.IsConnected)
87 | server.Disconnect();
88 | server.Dispose();
89 | });
90 | cancellationToken.ThrowIfCancellationRequested();
91 |
92 | await server.WaitForConnectionAsync(cancellationToken).ConfigureAwait(false);
93 |
94 | // we received a connection, which means another instance was started -> we bring the window to the front
95 | _ = Application.Current.Dispatcher.BeginInvoke(() => {
96 | Logger.LogInfo("App was started while already running -> bringing running window to front.");
97 | var mainWindow = (MainWindow)Application.Current.MainWindow;
98 | mainWindow.Dispatcher.BeginInvoke(() => mainWindow.Deminimize());
99 | });
100 |
101 | using var reader = new StreamReader(server);
102 | if (await reader.ReadLineAsync().ConfigureAwait(false) is string line && line == CliArgs.ProxyStartOption && !CliArgs.IsProxyStart) {
103 | StartWithSpotify.TransformToProxied();
104 | }
105 |
106 | server.Disconnect();
107 |
108 | // restart server
109 | await RunPipeServer(cancellationToken).ConfigureAwait(false);
110 | }
111 |
112 | private static int RunApp() {
113 | if (CliArgs.ForceDebugMode)
114 | App.ForceDebugMode = true;
115 |
116 | if (CliArgs.IsProxyStart)
117 | StartWithSpotify.HandleProxiedStart();
118 |
119 | var app = new App();
120 | app.InitializeComponent();
121 | app.ShutdownMode = ShutdownMode.OnMainWindowClose;
122 | app.DispatcherUnhandledException += (s, e) => OnUnhandledException(e.Exception);
123 | TaskScheduler.UnobservedTaskException += (s, e) =>
124 | Logger.LogException("Unobserved task exception: ", e.Exception);
125 |
126 | var exitCode = app.Run();
127 |
128 | if (CliArgs.IsProxyStart)
129 | StartWithSpotify.HandleProxiedExit();
130 |
131 | return exitCode;
132 | }
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/EZBlocker3/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace EZBlocker3.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("EZBlocker3.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 |
63 | ///
64 | /// Looks up a localized string similar to https://github.com/OpenByteDev/EZBlocker3.
65 | ///
66 | internal static string ProjectPageUrl {
67 | get {
68 | return ResourceManager.GetString("ProjectPageUrl", resourceCulture);
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/EZBlocker3/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 | https://github.com/OpenByteDev/EZBlocker3
122 |
123 |
--------------------------------------------------------------------------------
/EZBlocker3/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace EZBlocker3.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 |
26 | [global::System.Configuration.UserScopedSettingAttribute()]
27 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
28 | [global::System.Configuration.DefaultSettingValueAttribute("True")]
29 | public bool MinimizeToTray {
30 | get {
31 | return ((bool)(this["MinimizeToTray"]));
32 | }
33 | set {
34 | this["MinimizeToTray"] = value;
35 | }
36 | }
37 |
38 | [global::System.Configuration.UserScopedSettingAttribute()]
39 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
40 | [global::System.Configuration.DefaultSettingValueAttribute("True")]
41 | public bool UnmuteOnClose {
42 | get {
43 | return ((bool)(this["UnmuteOnClose"]));
44 | }
45 | set {
46 | this["UnmuteOnClose"] = value;
47 | }
48 | }
49 |
50 | [global::System.Configuration.UserScopedSettingAttribute()]
51 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
52 | [global::System.Configuration.DefaultSettingValueAttribute("True")]
53 | public bool CheckForUpdates {
54 | get {
55 | return ((bool)(this["CheckForUpdates"]));
56 | }
57 | set {
58 | this["CheckForUpdates"] = value;
59 | }
60 | }
61 |
62 | [global::System.Configuration.UserScopedSettingAttribute()]
63 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
64 | [global::System.Configuration.DefaultSettingValueAttribute("False")]
65 | public bool DebugMode {
66 | get {
67 | return ((bool)(this["DebugMode"]));
68 | }
69 | set {
70 | this["DebugMode"] = value;
71 | }
72 | }
73 |
74 | [global::System.Configuration.UserScopedSettingAttribute()]
75 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
76 | [global::System.Configuration.DefaultSettingValueAttribute("False")]
77 | public bool StartMinimized {
78 | get {
79 | return ((bool)(this["StartMinimized"]));
80 | }
81 | set {
82 | this["StartMinimized"] = value;
83 | }
84 | }
85 |
86 | [global::System.Configuration.UserScopedSettingAttribute()]
87 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
88 | [global::System.Configuration.DefaultSettingValueAttribute("False")]
89 | public bool StartOnLogin {
90 | get {
91 | return ((bool)(this["StartOnLogin"]));
92 | }
93 | set {
94 | this["StartOnLogin"] = value;
95 | }
96 | }
97 |
98 | [global::System.Configuration.UserScopedSettingAttribute()]
99 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
100 | [global::System.Configuration.DefaultSettingValueAttribute("")]
101 | public string IgnoreUpdate {
102 | get {
103 | return ((string)(this["IgnoreUpdate"]));
104 | }
105 | set {
106 | this["IgnoreUpdate"] = value;
107 | }
108 | }
109 |
110 | [global::System.Configuration.UserScopedSettingAttribute()]
111 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
112 | public global::System.Nullable MainWindowPosition {
113 | get {
114 | return ((global::System.Nullable)(this["MainWindowPosition"]));
115 | }
116 | set {
117 | this["MainWindowPosition"] = value;
118 | }
119 | }
120 |
121 | [global::System.Configuration.UserScopedSettingAttribute()]
122 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
123 | public global::System.Nullable VirtualScreenSize {
124 | get {
125 | return ((global::System.Nullable)(this["VirtualScreenSize"]));
126 | }
127 | set {
128 | this["VirtualScreenSize"] = value;
129 | }
130 | }
131 |
132 | [global::System.Configuration.UserScopedSettingAttribute()]
133 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
134 | [global::System.Configuration.DefaultSettingValueAttribute("True")]
135 | public bool UpgradeRequired {
136 | get {
137 | return ((bool)(this["UpgradeRequired"]));
138 | }
139 | set {
140 | this["UpgradeRequired"] = value;
141 | }
142 | }
143 |
144 | [global::System.Configuration.UserScopedSettingAttribute()]
145 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
146 | [global::System.Configuration.DefaultSettingValueAttribute("False")]
147 | public bool StartWithSpotify {
148 | get {
149 | return ((bool)(this["StartWithSpotify"]));
150 | }
151 | set {
152 | this["StartWithSpotify"] = value;
153 | }
154 | }
155 |
156 | [global::System.Configuration.UserScopedSettingAttribute()]
157 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
158 | [global::System.Configuration.DefaultSettingValueAttribute("")]
159 | public string AppPath {
160 | get {
161 | return ((string)(this["AppPath"]));
162 | }
163 | set {
164 | this["AppPath"] = value;
165 | }
166 | }
167 |
168 | [global::System.Configuration.UserScopedSettingAttribute()]
169 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
170 | public global::System.Nullable LastUpdateCheck {
171 | get {
172 | return ((global::System.Nullable)(this["LastUpdateCheck"]));
173 | }
174 | set {
175 | this["LastUpdateCheck"] = value;
176 | }
177 | }
178 |
179 | [global::System.Configuration.UserScopedSettingAttribute()]
180 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
181 | [global::System.Configuration.DefaultSettingValueAttribute("True")]
182 | public bool AggressiveMuting {
183 | get {
184 | return ((bool)(this["AggressiveMuting"]));
185 | }
186 | set {
187 | this["AggressiveMuting"] = value;
188 | }
189 | }
190 |
191 | [global::System.Configuration.UserScopedSettingAttribute()]
192 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
193 | [global::System.Configuration.DefaultSettingValueAttribute("ProcessAndWindowEventSpotifyHook")]
194 | public string Hook {
195 | get {
196 | return ((string)(this["Hook"]));
197 | }
198 | set {
199 | this["Hook"] = value;
200 | }
201 | }
202 |
203 | [global::System.Configuration.UserScopedSettingAttribute()]
204 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
205 | [global::System.Configuration.DefaultSettingValueAttribute("mute")]
206 | public string BlockType {
207 | get {
208 | return ((string)(this["BlockType"]));
209 | }
210 | set {
211 | this["BlockType"] = value;
212 | }
213 | }
214 | }
215 | }
216 |
--------------------------------------------------------------------------------
/EZBlocker3/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | True
7 |
8 |
9 | True
10 |
11 |
12 | True
13 |
14 |
15 | False
16 |
17 |
18 | False
19 |
20 |
21 | False
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | True
34 |
35 |
36 | False
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | True
46 |
47 |
48 | ProcessAndWindowEventSpotifyHook
49 |
50 |
51 | mute
52 |
53 |
54 |
--------------------------------------------------------------------------------
/EZBlocker3/Settings/Autostart.cs:
--------------------------------------------------------------------------------
1 | using EZBlocker3.Logging;
2 | using Microsoft.Win32;
3 |
4 | namespace EZBlocker3.Settings {
5 | public static class Autostart {
6 | private const string StartupApprovedRunKey = @"Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run";
7 | private const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
8 |
9 | public static bool? IsEnabled() {
10 | using var runKey = Registry.CurrentUser.OpenSubKey(RunKey, writable: false);
11 | var runKeyValue = runKey?.GetValue(App.ProductName);
12 |
13 | using var startupApprovedRunKey = Registry.CurrentUser.OpenSubKey(StartupApprovedRunKey, writable: false);
14 | var startupApprovedRunKeyValue = startupApprovedRunKey?.GetValue(App.ProductName);
15 |
16 | return ((runKeyValue, startupApprovedRunKeyValue)) switch {
17 | (not null, not null) => true,
18 | (null, null) => false,
19 | _ => null
20 | };
21 | }
22 |
23 | public static void Enable() {
24 | using var runKey = Registry.CurrentUser.OpenSubKey(RunKey, writable: true);
25 | runKey?.SetValue(App.ProductName, $"{App.Location} {CliArgs.AutomaticStartOption}", RegistryValueKind.String);
26 |
27 | using var startupApprovedRunKey = Registry.CurrentUser.OpenSubKey(StartupApprovedRunKey, writable: true);
28 | startupApprovedRunKey?.SetValue(App.ProductName, new byte[] { 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, RegistryValueKind.Binary);
29 |
30 | Logger.LogInfo("Settings: Enabled Autostart");
31 | }
32 |
33 | public static void Disable() {
34 | using var runKey = Registry.CurrentUser.OpenSubKey(RunKey, writable: true);
35 | runKey?.DeleteValue(App.ProductName, throwOnMissingValue: false);
36 |
37 | using var startupApprovedRunKey = Registry.CurrentUser.OpenSubKey(StartupApprovedRunKey, writable: true);
38 | startupApprovedRunKey?.DeleteValue(App.ProductName, throwOnMissingValue: false);
39 |
40 | Logger.LogInfo("Settings: Disabled Autostart");
41 | }
42 |
43 | public static void SetEnabled(bool enabled) {
44 | if (enabled)
45 | Enable();
46 | else
47 | Disable();
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/EZBlocker3/Settings/SettingsWindow.xaml:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/EZBlocker3/Settings/SettingsWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 | using MessageBox = ModernWpf.MessageBox;
3 |
4 | namespace EZBlocker3.Settings {
5 | public partial class SettingsWindow : Window {
6 | public SettingsWindow() {
7 | InitializeComponent();
8 |
9 | unmuteOnCloseCheckBox.IsChecked = Properties.Settings.Default.UnmuteOnClose;
10 | minimizeToTrayRadioButton.IsChecked = Properties.Settings.Default.MinimizeToTray;
11 | minimizeToTaskbarRadioButton.IsChecked = !Properties.Settings.Default.MinimizeToTray;
12 | checkForUpdatesCheckBox.IsChecked = Properties.Settings.Default.CheckForUpdates;
13 | debugModeCheckBox.IsChecked = Properties.Settings.Default.DebugMode;
14 | aggressiveMutingCheckBox.IsChecked = Properties.Settings.Default.AggressiveMuting;
15 | startMinimizedCheckBox.IsChecked = Properties.Settings.Default.StartMinimized;
16 | startOnLoginCheckBox.IsChecked = Properties.Settings.Default.StartOnLogin;
17 | startWithSpotifyCheckBox.IsChecked = Properties.Settings.Default.StartWithSpotify;
18 | mediaControlHookButton.IsChecked = Equals(Properties.Settings.Default.Hook, mediaControlHookButton.Tag);
19 | processAndWindowHookButton.IsChecked = !mediaControlHookButton.IsChecked; // Equals(Properties.Settings.Default.Hook, processAndWindowHookButton.Tag);
20 | skipBlockTypeButton.IsChecked = Equals(Properties.Settings.Default.BlockType, skipBlockTypeButton.Tag);
21 | muteBlockTypeButton.IsChecked = !skipBlockTypeButton.IsChecked; // Equals(Properties.Settings.Default.BlockType, muteBlockTypeButton.Tag);
22 |
23 | startWithSpotifyCheckBox.IsEnabled = StartWithSpotify.Available;
24 |
25 | mediaControlHookButton.Checked += (_, __) => {
26 | if (mediaControlHookButton.IsChecked == true) {
27 | muteBlockTypeButton.IsChecked = false;
28 | skipBlockTypeButton.IsChecked = true;
29 | }
30 | };
31 |
32 | saveButton.Click += (_, __) => { SaveSettings(); Close(); };
33 | cancelButton.Click += (_, __) => Close();
34 | uninstallButton.Click += (_, __) => {
35 | if (MessageBox.Show("Do you really want to uninstall EZBlocker 3?", "Confirm Uninstall", MessageBoxButton.YesNo) == MessageBoxResult.Yes) {
36 | Uninstall.Run();
37 | }
38 | };
39 | }
40 |
41 | private void SaveSettings() {
42 | if (!App.SaveSettingsOnClose)
43 | return;
44 |
45 | var hookBefore = Properties.Settings.Default.Hook;
46 | var blockTypeBefore = Properties.Settings.Default.BlockType;
47 |
48 | Properties.Settings.Default.UnmuteOnClose = unmuteOnCloseCheckBox.IsChecked ?? Properties.Settings.Default.UnmuteOnClose;
49 | Properties.Settings.Default.MinimizeToTray = minimizeToTrayRadioButton.IsChecked ?? Properties.Settings.Default.MinimizeToTray;
50 | Properties.Settings.Default.CheckForUpdates = checkForUpdatesCheckBox.IsChecked ?? Properties.Settings.Default.CheckForUpdates;
51 | Properties.Settings.Default.DebugMode = debugModeCheckBox.IsChecked ?? Properties.Settings.Default.DebugMode;
52 | Properties.Settings.Default.AggressiveMuting = aggressiveMutingCheckBox.IsChecked ?? Properties.Settings.Default.AggressiveMuting;
53 | Properties.Settings.Default.StartMinimized = startMinimizedCheckBox.IsChecked ?? Properties.Settings.Default.StartMinimized;
54 | Properties.Settings.Default.StartOnLogin = startOnLoginCheckBox.IsChecked ?? Properties.Settings.Default.StartOnLogin;
55 | Properties.Settings.Default.StartWithSpotify = startWithSpotifyCheckBox.IsChecked ?? Properties.Settings.Default.StartWithSpotify;
56 | Properties.Settings.Default.Hook = mediaControlHookButton.IsChecked == true ? (string)mediaControlHookButton.Tag : (string)processAndWindowHookButton.Tag;
57 | Properties.Settings.Default.BlockType = skipBlockTypeButton.IsChecked == true ? (string)skipBlockTypeButton.Tag : (string)muteBlockTypeButton.Tag;
58 |
59 | Autostart.SetEnabled(Properties.Settings.Default.StartOnLogin);
60 | if (StartWithSpotify.Available)
61 | StartWithSpotify.SetEnabled(Properties.Settings.Default.StartWithSpotify);
62 |
63 | Properties.Settings.Default.Save();
64 |
65 | if (hookBefore != Properties.Settings.Default.Hook || blockTypeBefore != Properties.Settings.Default.BlockType) {
66 | MessageBox.Show("You need to restart EZBlocker 3 for the changes to take effect.", "Restart required", MessageBoxButton.OK, MessageBoxImage.Information);
67 | }
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/EZBlocker3/Settings/StartWithSpotify.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.CodeDom.Compiler;
3 | using System.Diagnostics;
4 | using System.Drawing;
5 | using System.IO;
6 | using EZBlocker3.Logging;
7 | using EZBlocker3.Utils;
8 | using Lazy;
9 |
10 | namespace EZBlocker3.Settings {
11 | public static class StartWithSpotify {
12 | public static readonly string SpotifyPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + @"\Spotify\Spotify.exe";
13 | private static readonly string RealSpotifyPath = Path.ChangeExtension(SpotifyPath, "real.exe");
14 | private static readonly string ProxyTempPath = Path.ChangeExtension(SpotifyPath, "proxy.exe");
15 | [Lazy]
16 | private static bool IsSpotifyDesktopInstalled => File.Exists(SpotifyPath);
17 |
18 | public static bool Available => IsSpotifyDesktopInstalled;
19 |
20 | public static void SetEnabled(bool enabled) {
21 | if (enabled)
22 | Enable();
23 | else
24 | Disable();
25 | }
26 | public static void Enable() {
27 | if (!IsSpotifyDesktopInstalled)
28 | return;
29 |
30 | if (IsProxyInstalled())
31 | return;
32 | InstallProxy();
33 | }
34 | public static void Disable() {
35 | if (!IsSpotifyDesktopInstalled)
36 | return;
37 |
38 | if (!IsProxyInstalled())
39 | return;
40 | UninstallProxy();
41 | }
42 |
43 | private static bool IsProxyInstalled() {
44 | if (File.Exists(RealSpotifyPath))
45 | return true;
46 | if (Program.CliArgs.IsProxyStart && File.Exists(ProxyTempPath))
47 | return true;
48 | return !IsInvalidStateAfterSpotifyUpdate();
49 | }
50 | private static void InstallProxy() {
51 | HandleInvalidStateAfterUpdate();
52 |
53 | var tempIconFilePath = Path.ChangeExtension(SpotifyPath, ".ico.temp");
54 | try {
55 | using var spotifyIcon = Icon.ExtractAssociatedIcon(SpotifyPath);
56 | using var spotifyIconBitmap = spotifyIcon.ToBitmap();
57 | File.Delete(tempIconFilePath);
58 | BitmapUtils.SaveAsIcon(spotifyIconBitmap, tempIconFilePath);
59 |
60 | File.Delete(ProxyTempPath);
61 | if (GenerateProxy(ProxyTempPath, tempIconFilePath)) {
62 | Logger.Proxy.LogInfo("Settings: Successfully generated proxy executable");
63 | File.Move(SpotifyPath, RealSpotifyPath);
64 | File.Move(ProxyTempPath, SpotifyPath);
65 | } else {
66 | Logger.Proxy.LogError("Settings: Failed to generate proxy executable");
67 | }
68 | } catch {
69 | File.Delete(ProxyTempPath);
70 | throw;
71 | } finally {
72 | File.Delete(tempIconFilePath);
73 | }
74 | }
75 | private static void UninstallProxy() {
76 | HandleInvalidStateAfterUpdate();
77 |
78 | if (File.Exists(RealSpotifyPath)) {
79 | // spotify is not running
80 | File.Delete(SpotifyPath);
81 | File.Move(RealSpotifyPath, SpotifyPath);
82 | } else {
83 | // spotify is running
84 | File.Delete(ProxyTempPath);
85 | }
86 | }
87 |
88 | private static bool IsInvalidStateAfterSpotifyUpdate() {
89 | // check if executable is smaller than 5MB
90 | // the real spotify executable is > 20MB and the proxy should be less than 1MB
91 | // if the size is > 5MB that means that spotify was updated and replaced the proxy
92 | return new FileInfo(SpotifyPath).Length > 1024 * 1024 * 5;
93 | }
94 | private static void HandleInvalidStateAfterUpdate() {
95 | if (!IsInvalidStateAfterSpotifyUpdate())
96 | return;
97 |
98 | try {
99 | File.Delete(RealSpotifyPath);
100 | } catch {
101 | File.Move(RealSpotifyPath, Path.GetTempFileName());
102 | }
103 | }
104 |
105 | private static bool GenerateProxy(string executablePath, string iconPath) {
106 | var parameters = new CompilerParameters {
107 | GenerateExecutable = true,
108 | OutputAssembly = executablePath,
109 | GenerateInMemory = false
110 | };
111 | parameters.ReferencedAssemblies.Add("System.dll");
112 | parameters.CompilerOptions = $"/target:winexe \"/win32icon:{iconPath}\"";
113 |
114 | var provider = CodeDomProvider.CreateProvider("CSharp");
115 | var code = GetProxyCode(App.Location, appArgs: CliArgs.ProxyStartOption, RealSpotifyPath, spotifyArgs: string.Empty);
116 | var result = provider.CompileAssemblyFromSource(parameters, code);
117 |
118 | foreach (CompilerError error in result.Errors)
119 | Logger.Proxy.LogWarning($"Settings: Redirection executable generation {(error.IsWarning ? "warning" : "error")}:\n{error.ErrorText}");
120 |
121 | return result.Errors.Count == 0;
122 | }
123 |
124 | public static void TransformToProxied() {
125 | if (Program.CliArgs.IsProxyStart)
126 | throw new InvalidOperationException("Already running as proxied.");
127 |
128 | if (!IsSpotifyDesktopInstalled)
129 | return;
130 |
131 | Program.CliArgs = Program.CliArgs with { IsProxyStart = true };
132 | HandleProxiedStart();
133 | }
134 | public static void HandleProxiedStart() {
135 | Logger.Proxy.LogInfo("Started through proxy executable");
136 |
137 | if (!File.Exists(RealSpotifyPath)) {
138 | Logger.Proxy.LogWarning("Started through proxy executable when no proxy is present");
139 | return;
140 | }
141 |
142 | try {
143 | File.Delete(ProxyTempPath);
144 | File.Move(SpotifyPath, ProxyTempPath);
145 | File.Move(RealSpotifyPath, SpotifyPath);
146 | StartSpotify();
147 | } catch (Exception e) {
148 | Logger.Proxy.LogException("Failed to handle proxied start:", e);
149 | }
150 | }
151 | public static void HandleProxiedExit() {
152 | if (!IsSpotifyDesktopInstalled)
153 | return;
154 |
155 | Logger.Proxy.LogInfo("Reset proxy executable");
156 |
157 | if (!File.Exists(ProxyTempPath)) {
158 | Logger.Proxy.LogWarning("Failed to reset proxy as no proxy is present");
159 | return;
160 | }
161 |
162 | try {
163 | File.Move(SpotifyPath, RealSpotifyPath);
164 | File.Move(ProxyTempPath, SpotifyPath);
165 | } catch (Exception e) {
166 | Logger.Proxy.LogException("Failed to handle proxied exit:", e);
167 | }
168 | }
169 | public static void StartSpotify(bool ignoreProxy = false) {
170 | if (!IsSpotifyDesktopInstalled)
171 | return;
172 |
173 | if (ignoreProxy && File.Exists(RealSpotifyPath)) {
174 | Process.Start(RealSpotifyPath).Dispose();
175 | return;
176 | }
177 |
178 | Process.Start(SpotifyPath).Dispose();
179 | }
180 |
181 | private static string GetProxyCode(string appPath, string appArgs, string spotifyPath, string spotifyArgs) => @"
182 | using System.Diagnostics;
183 |
184 | public static class Proxy {
185 | public static void Main() {
186 | var appPath = @""" + appPath + @""";
187 | var appArgs = @""" + appArgs + @""";
188 | var spotifyPath = @""" + spotifyPath + @""";
189 | var spotifyArgs = @""" + spotifyArgs + @""";
190 |
191 | // Process.Start(spotifyPath, spotifyArgs).Dispose();
192 | Process.Start(appPath, appArgs).Dispose();
193 | }
194 | }";
195 | }
196 | }
197 |
198 |
--------------------------------------------------------------------------------
/EZBlocker3/Settings/Uninstall.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Windows;
6 | using EZBlocker3.Extensions;
7 |
8 | namespace EZBlocker3.Settings {
9 | public static class Uninstall {
10 | public static void Run(bool deleteSettings = false) {
11 | Autostart.Disable();
12 | StartWithSpotify.Disable();
13 |
14 | if (deleteSettings) {
15 | App.SaveSettingsOnClose = false;
16 | DeleteSettings();
17 | }
18 |
19 | Process.Start(new ProcessStartInfo() {
20 | FileName = "cmd.exe",
21 | Arguments = "/C choice /C Y /N /D Y /T 5 & DEL " + App.Location,
22 | WindowStyle = ProcessWindowStyle.Hidden,
23 | CreateNoWindow = true,
24 | });
25 |
26 | Application.Current?.Dispatcher.Invoke(() => Application.Current.Shutdown());
27 |
28 | void DeleteSettings() {
29 | var appExecutableName = Path.GetFileName(App.Location);
30 | var appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
31 | var containerPath = Path.Combine(appDataPath, App.CompanyName);
32 | var containerDiretory = new DirectoryInfo(containerPath);
33 |
34 | var settingsDirectories = containerDiretory.GetDirectories()
35 | .Where(directory => directory.Name.StartsWith(appExecutableName));
36 |
37 | foreach (var settingsDir in settingsDirectories)
38 | settingsDir.RecursiveDelete();
39 |
40 | if (!containerDiretory.EnumerateDirectories().Any() && !containerDiretory.EnumerateFiles().Any())
41 | containerDiretory.Delete();
42 | }
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/EZBlocker3/Spotify/AbstractSpotifyAdBlocker.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace EZBlocker3.Spotify {
4 | public abstract class AbstractSpotifyAdBlocker : IActivatable {
5 | public bool IsActive { get; private set; }
6 | public ISpotifyHook Hook { get; }
7 |
8 | protected AbstractSpotifyAdBlocker(ISpotifyHook hook) {
9 | Hook = hook;
10 | }
11 |
12 | public void Activate() {
13 | if (IsActive)
14 | throw new InvalidOperationException("Blocker is already active.");
15 |
16 | IsActive = true;
17 |
18 | Hook.HookChanged += OnHookChanged;
19 | Hook.SpotifyStateChanged += OnSpotifyStateChanged;
20 | Hook.ActiveSongChanged += OnActiveSongChanged;
21 | }
22 |
23 | public void Deactivate() {
24 | if (!IsActive)
25 | throw new InvalidOperationException("Hook has to be active.");
26 |
27 | IsActive = false;
28 |
29 | Hook.HookChanged -= OnHookChanged;
30 | Hook.SpotifyStateChanged -= OnSpotifyStateChanged;
31 | Hook.ActiveSongChanged -= OnActiveSongChanged;
32 | }
33 |
34 | protected virtual void OnHookChanged(object sender, EventArgs e) { }
35 | protected virtual void OnSpotifyStateChanged(object sender, SpotifyStateChangedEventArgs e) { }
36 | protected virtual void OnActiveSongChanged(object sender, ActiveSongChangedEventArgs e) { }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/EZBlocker3/Spotify/AbstractSpotifyHook.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using EZBlocker3.Logging;
3 |
4 | namespace EZBlocker3.Spotify {
5 | public abstract class AbstractSpotifyHook : ISpotifyHook {
6 | private bool isActive;
7 | ///
8 | /// Gets a value indicating whether this object is active and trying to hook spotify.
9 | ///
10 | public bool IsActive {
11 | get => isActive;
12 | protected set {
13 | isActive = value;
14 | if (!isActive) {
15 | IsHooked = false;
16 | }
17 | }
18 | }
19 |
20 | private bool isHooked;
21 | ///
22 | /// Gets a value indicating whether spotify is currently hooked.
23 | ///
24 | public bool IsHooked {
25 | get => isHooked;
26 | protected set {
27 | var oldValue = isHooked;
28 | isHooked = value;
29 | if (!isHooked) {
30 | State = SpotifyState.Unknown;
31 | ActiveSong = null;
32 | }
33 | if (oldValue != value) {
34 | RaiseHookChanged();
35 | }
36 | }
37 | }
38 |
39 | ///
40 | /// Gets the currently playing song or null if no song is being played or spotify is not running.
41 | ///
42 | public SongInfo? ActiveSong { get; private set; }
43 | ///
44 | /// Gets a value indicating the current state of a running spotify process.
45 | ///
46 | public SpotifyState State { get; private set; } // = SpotifyState.Unknown;
47 |
48 | ///
49 | /// Gets a value indicating whether spotify is currently paused.
50 | ///
51 | public bool IsPaused => State == SpotifyState.Paused;
52 | ///
53 | /// Gets a value indicating whether spotify is currently playing.
54 | ///
55 | public bool IsPlaying => IsSongPlaying || IsAdPlaying;
56 | ///
57 | /// Gets a value indicating whether spotify is currently playing a song.
58 | ///
59 | public bool IsSongPlaying => State == SpotifyState.PlayingSong;
60 | ///
61 | /// Gets a value indicating whether spotify is currently playing an advertisement.
62 | ///
63 | public bool IsAdPlaying => State == SpotifyState.PlayingAdvertisement;
64 |
65 | ///
66 | /// Occurs whenever the currently playing song changes.
67 | ///
68 | public event EventHandler? ActiveSongChanged;
69 | ///
70 | /// Occurs whenever a new spotify process is hooked or an exisiting one is unhooked.
71 | ///
72 | public event EventHandler? HookChanged;
73 | ///
74 | /// Occurs whenever spotify changes its state.
75 | ///
76 | public event EventHandler? SpotifyStateChanged;
77 |
78 | ///
79 | public abstract void Activate();
80 |
81 | ///
82 | public abstract void Deactivate();
83 |
84 | ///
85 | /// Updates the current state of the spotify process.
86 | ///
87 | /// The new state of the spotify process.
88 | /// The currently playing song, if applicable.
89 | protected void UpdateSpotifyState(SpotifyState newState, SongInfo? newSong = null, bool forceRaiseEvents = false) {
90 | var prevSong = ActiveSong;
91 | var prevState = State;
92 | ActiveSong = newSong;
93 | State = newState;
94 |
95 | if (forceRaiseEvents || prevState != newState)
96 | RaiseSpotifyStateChanged(prevState, newState);
97 | if (forceRaiseEvents || !Equals(prevSong, newSong))
98 | RaiseActiveSongChanged(prevSong, newSong);
99 | }
100 |
101 | private void RaiseActiveSongChanged(SongInfo? previous, SongInfo? current) =>
102 | OnActiveSongChanged(new ActiveSongChangedEventArgs(previous, current));
103 | ///
104 | /// OnActiveSongChanged is called whenever the currently active song changes.
105 | ///
106 | ///
107 | protected virtual void OnActiveSongChanged(ActiveSongChangedEventArgs eventArgs) {
108 | Logger.Hook.LogInfo($"Active song: \"{eventArgs.NewActiveSong}\"");
109 | ActiveSongChanged?.Invoke(this, eventArgs);
110 | }
111 |
112 | private void RaiseHookChanged() =>
113 | OnHookChanged(EventArgs.Empty);
114 | ///
115 | /// OnHookChanged is called whenever spotify is hooked or unhooked.
116 | ///
117 | ///
118 | protected virtual void OnHookChanged(EventArgs eventArgs) {
119 | Logger.Hook.LogInfo($"Spotify {(IsHooked ? "hooked" : "unhooked")}.");
120 | HookChanged?.Invoke(this, eventArgs);
121 | }
122 |
123 | private void RaiseSpotifyStateChanged(SpotifyState previous, SpotifyState current) =>
124 | OnSpotifyStateChanged(new SpotifyStateChangedEventArgs(previous, current));
125 | ///
126 | /// OnSpotifyStateChanged is called whenever the current state of spotify changes.
127 | ///
128 | ///
129 | protected virtual void OnSpotifyStateChanged(SpotifyStateChangedEventArgs eventArgs) {
130 | Logger.Hook.LogInfo($"Spotify is in {eventArgs.NewState} state.");
131 | SpotifyStateChanged?.Invoke(this, eventArgs);
132 | }
133 |
134 | #region IDisposable
135 | protected virtual void Dispose(bool disposing) { }
136 |
137 | public void Dispose() {
138 | Dispose(disposing: true);
139 | GC.SuppressFinalize(this);
140 | }
141 | #endregion
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/EZBlocker3/Spotify/GlobalSystemMediaTransportControlSpotifyHook.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using EZBlocker3.Extensions;
4 | using EZBlocker3.Logging;
5 | using Windows.Media.Control;
6 | using Manager = Windows.Media.Control.GlobalSystemMediaTransportControlsSessionManager;
7 | using Session = Windows.Media.Control.GlobalSystemMediaTransportControlsSession;
8 |
9 | namespace EZBlocker3.Spotify {
10 | public class GlobalSystemMediaTransportControlSpotifyHook : AbstractSpotifyHook {
11 | private Manager? manager;
12 | private Session? session;
13 |
14 | public override void Activate() {
15 | if (IsActive)
16 | throw new InvalidOperationException("Hook is already active.");
17 |
18 | Logger.Hook.LogDebug("Activated");
19 |
20 | IsActive = true;
21 |
22 | // awaiting here hangs the app?
23 | manager = Manager.RequestAsync().AsTask().GetAwaiter().GetResult();
24 |
25 | SetupHook();
26 | TryHook();
27 | }
28 |
29 | public override void Deactivate() {
30 | if (!IsActive)
31 | throw new InvalidOperationException("Hook has to be active.");
32 |
33 | IsActive = false;
34 |
35 | ClearHook();
36 |
37 | Logger.Hook.LogDebug("Deactivated");
38 | }
39 |
40 | private bool TryHook() {
41 | if (IsHooked)
42 | return true;
43 |
44 | var sessions = manager!.GetSessions().ToArray();
45 |
46 | var exactMatch = sessions.Where(e => e.SourceAppUserModelId.Equals("Spotify.exe", StringComparison.OrdinalIgnoreCase));
47 | var fuzzyMatch = sessions.Where(e => e.SourceAppUserModelId.Contains("spotify", StringComparison.OrdinalIgnoreCase));
48 |
49 | var spotifySession = exactMatch.Concat(fuzzyMatch).FirstOrDefault();
50 |
51 | if (spotifySession == null)
52 | return false;
53 |
54 | HookSession(spotifySession);
55 | return true;
56 | }
57 |
58 | private void SetupHook() {
59 | manager!.SessionsChanged += Manager_SessionsChanged;
60 | }
61 |
62 | private void ClearHook() {
63 | if (manager != null) {
64 | manager.SessionsChanged -= Manager_SessionsChanged;
65 | }
66 | session = null;
67 | }
68 |
69 | private void Manager_SessionsChanged(object sender, SessionsChangedEventArgs args) {
70 | if (IsHooked) {
71 | if (!manager!.GetSessions().Contains(session)) {
72 | if (session != null) {
73 | session.MediaPropertiesChanged -= SpotifySession_MediaPropertiesChanged;
74 | session.PlaybackInfoChanged -= SpotifySession_PlaybackInfoChanged;
75 | session = null;
76 | }
77 | IsHooked = false;
78 | TryHook();
79 | }
80 | } else {
81 | TryHook();
82 | }
83 | }
84 |
85 | private void HookSession(Session spotifySession) {
86 | Logger.Hook.LogInfo("Hooked Session");
87 | session = spotifySession;
88 |
89 | session.MediaPropertiesChanged += SpotifySession_MediaPropertiesChanged;
90 | session.PlaybackInfoChanged += SpotifySession_PlaybackInfoChanged;
91 |
92 | IsHooked = true;
93 | HandleSpotifyStateChanged();
94 | }
95 |
96 | private void SpotifySession_PlaybackInfoChanged(object sender, PlaybackInfoChangedEventArgs args) {
97 | HandleSpotifyStateChanged();
98 | }
99 |
100 | private void SpotifySession_MediaPropertiesChanged(object sender, MediaPropertiesChangedEventArgs args) {
101 | HandleSpotifyStateChanged();
102 | }
103 |
104 | private void HandleSpotifyStateChanged() {
105 | if (session is null) {
106 | Logger.Hook.LogError("Spotify Media Session is null when it shouldn't.");
107 | return;
108 | }
109 |
110 | try {
111 | var mediaProperties = session.TryGetMediaPropertiesAsync().AsTask().GetAwaiter().GetResult();
112 | var playbackInfo = session.GetPlaybackInfo();
113 |
114 | var title = mediaProperties.Title;
115 | var artist = mediaProperties.Artist;
116 |
117 | Logger.Hook.LogDebug($"Media Properties: (Title: \"{title}\", Artist: \"{artist}\")");
118 | Logger.Hook.LogDebug($"PlaybackInfo: (Status: \"{playbackInfo.PlaybackStatus}\")");
119 |
120 | var isEmptyMedia = string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(artist);
121 | var isAd = artist == "Spotify" || artist == "Sponsored Message" || title == "Advertisement" || title == "Spotify";
122 |
123 | var state = playbackInfo.PlaybackStatus switch {
124 | GlobalSystemMediaTransportControlsSessionPlaybackStatus.Playing =>
125 | isAd ? SpotifyState.PlayingAdvertisement : (isEmptyMedia ? State : SpotifyState.PlayingSong),
126 | GlobalSystemMediaTransportControlsSessionPlaybackStatus.Paused => SpotifyState.Paused,
127 | GlobalSystemMediaTransportControlsSessionPlaybackStatus.Opened => SpotifyState.StartingUp,
128 | GlobalSystemMediaTransportControlsSessionPlaybackStatus.Closed => SpotifyState.ShuttingDown,
129 | _ => SpotifyState.Unknown
130 | };
131 |
132 | var song = !isAd && !isEmptyMedia ? new SongInfo(title, artist) : null;
133 |
134 | UpdateSpotifyState(state, song);
135 | } catch (Exception e) {
136 | Logger.Hook.LogException("Error while trying to determined spotify state", e);
137 | }
138 | }
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/EZBlocker3/Spotify/IActivatable.cs:
--------------------------------------------------------------------------------
1 | namespace EZBlocker3.Spotify {
2 | public interface IActivatable {
3 | bool IsActive { get; }
4 | void Activate();
5 | void Deactivate();
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/EZBlocker3/Spotify/IMutingSpotifyHook.cs:
--------------------------------------------------------------------------------
1 | namespace EZBlocker3.Spotify {
2 | public interface IMutingSpotifyHook {
3 | ///
4 | /// Sets the spotify mute status to the given state.
5 | ///
6 | /// A value indicating whether spotify should be muted or unmuted.
7 | /// A value indicating whether the operation was successful
8 | bool SetMute(bool mute);
9 | }
10 |
11 | public static class ISpotifyMuterExtensions {
12 | ///
13 | /// Mutes the spotify audio session.
14 | ///
15 | /// A value indicating whether the operation was successful
16 | public static bool Mute(this IMutingSpotifyHook muter) => muter.SetMute(true);
17 | ///
18 | /// Unmutes the spotify audio session.
19 | ///
20 | /// A value indicating whether the operation was successful
21 | public static bool Unmute(this IMutingSpotifyHook muter) => muter.SetMute(false);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/EZBlocker3/Spotify/ISpotifyHook.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace EZBlocker3.Spotify {
4 | public interface ISpotifyHook : IActivatable, IDisposable {
5 | ///
6 | /// Gets the currently playing song or null if no song is being played or spotify is not running.
7 | ///
8 | public SongInfo? ActiveSong { get; }
9 | ///
10 | /// Gets a value indicating the current state of a running spotify process.
11 | ///
12 | public SpotifyState State { get; }
13 | ///
14 | /// Gets a value indicating the current state of a running spotify process.
15 | ///
16 | public bool IsHooked { get; }
17 |
18 | ///
19 | /// Occurs whenever the currently playing song changes.
20 | ///
21 | public event EventHandler? ActiveSongChanged;
22 | ///
23 | /// Occurs whenever a new spotify process is hooked or an exisiting one is unhooked.
24 | ///
25 | public event EventHandler? HookChanged;
26 | ///
27 | /// Occurs whenever spotify changes its state.
28 | ///
29 | public event EventHandler? SpotifyStateChanged;
30 | }
31 |
32 | #region EventArgs
33 | public class SpotifyStateChangedEventArgs : EventArgs {
34 | public SpotifyState PreviousState { get; }
35 | public SpotifyState NewState { get; }
36 |
37 | public SpotifyStateChangedEventArgs(SpotifyState previousState, SpotifyState newState) {
38 | PreviousState = previousState;
39 | NewState = newState;
40 | }
41 | }
42 |
43 | public class ActiveSongChangedEventArgs : EventArgs {
44 | public SongInfo? PreviousActiveSong { get; }
45 | public SongInfo? NewActiveSong { get; }
46 |
47 | public ActiveSongChangedEventArgs(SongInfo? previousActiveSong, SongInfo? newActiveSong) {
48 | PreviousActiveSong = previousActiveSong;
49 | NewActiveSong = newActiveSong;
50 | }
51 | }
52 | #endregion
53 | }
54 |
--------------------------------------------------------------------------------
/EZBlocker3/Spotify/MutingSpotifyAdBlocker.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | namespace EZBlocker3.Spotify {
4 | public class MutingSpotifyAdBlocker : AbstractSpotifyAdBlocker {
5 | private readonly IMutingSpotifyHook muter;
6 |
7 | public bool WaitForAudioFade { get; set; } = true;
8 | public bool AggressiveMuting { get; set; } = false;
9 |
10 | public MutingSpotifyAdBlocker(ProcessAndWindowEventSpotifyHook hook) : this(hook, hook) { }
11 | public MutingSpotifyAdBlocker(ISpotifyHook hook, IMutingSpotifyHook muter) : base(hook) {
12 | this.muter = muter;
13 | }
14 |
15 | protected override void OnSpotifyStateChanged(object sender, SpotifyStateChangedEventArgs eventArgs) {
16 | base.OnSpotifyStateChanged(sender, eventArgs);
17 |
18 | var oldState = eventArgs.PreviousState;
19 | var newState = eventArgs.NewState;
20 |
21 | if (newState == SpotifyState.StartingUp || newState == SpotifyState.ShuttingDown)
22 | return;
23 |
24 | // we skip here as no audio session is present and muting would fail.
25 | if (oldState == SpotifyState.StartingUp && newState == SpotifyState.Paused)
26 | return;
27 |
28 | if (AggressiveMuting) {
29 | muter.SetMute(newState != SpotifyState.PlayingSong);
30 | return;
31 | }
32 |
33 | if (!WaitForAudioFade || oldState != SpotifyState.PlayingAdvertisement) {
34 | muter.SetMute(mute: newState == SpotifyState.PlayingAdvertisement);
35 | return;
36 | }
37 |
38 | if (newState == SpotifyState.PlayingAdvertisement) {
39 | muter.Mute();
40 | return;
41 | }
42 |
43 | Task.Run(async () => {
44 | await Task.Delay(600).ConfigureAwait(false);
45 | muter.Unmute();
46 | });
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/EZBlocker3/Spotify/ProcessAndWindowEventSpotifyHook.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Diagnostics;
3 | using System.Linq;
4 | using System.Linq.Expressions;
5 | using System.Reflection;
6 | using EZBlocker3.Audio.CoreAudio;
7 | using EZBlocker3.Extensions;
8 | using EZBlocker3.Interop;
9 | using EZBlocker3.Logging;
10 | using EZBlocker3.Utils;
11 | using Lazy;
12 | using Microsoft.Windows.Sdk;
13 | using WinEventHook;
14 |
15 | namespace EZBlocker3.Spotify {
16 | ///
17 | /// Represents a hook to the spotify process.
18 | ///
19 | public class ProcessAndWindowEventSpotifyHook : AbstractSpotifyHook, IMutingSpotifyHook {
20 | ///
21 | /// The main window process if spotify is running.
22 | ///
23 | public Process? MainWindowProcess { get; private set; }
24 |
25 | ///
26 | /// The main window handle if spotify is running.
27 | ///
28 | public IntPtr? MainWindowHandle { get; private set; }
29 |
30 | ///
31 | /// The current window title of the main window.
32 | ///
33 | public string? WindowTitle { get; private set; }
34 |
35 | ///
36 | /// Gets a value indicating whether spotify is currently muting or null if spotify is not running.
37 | ///
38 | public bool? IsMuted => AudioSession?.IsMuted;
39 |
40 | ///
41 | /// The current audio session if spotify is running and a session has been initialized.
42 | ///
43 | private AudioSession? _audioSession;
44 | public AudioSession? AudioSession => _audioSession ??= FetchAudioSession();
45 |
46 | public bool AssumeAdOnUnknownState { get; init; }
47 |
48 | private readonly WindowEventHook _titleChangeEventHook = new(WindowEvent.EVENT_OBJECT_NAMECHANGE);
49 | private readonly WindowEventHook _windowDestructionEventHook = new(WindowEvent.EVENT_OBJECT_DESTROY);
50 | private readonly WindowEventHook _windowCreationEventHook = new(WindowEvent.EVENT_OBJECT_SHOW);
51 |
52 | private readonly ReentrancySafeEventProcessor _windowCreationEventProcessor;
53 | private readonly ReentrancySafeEventProcessor _titleChangeEventProcessor;
54 | private readonly ReentrancySafeEventProcessor _windowDestructionEventProcessor;
55 |
56 | ///
57 | /// Creates a new inactive spotify hook.
58 | ///
59 | public ProcessAndWindowEventSpotifyHook() {
60 | _windowCreationEventHook.EventReceived += WindowCreationEventReceived;
61 | _titleChangeEventHook.EventReceived += TitleChangeEventReceived;
62 | _windowDestructionEventHook.EventReceived += WindowDestructionEventReceived;
63 |
64 | _windowCreationEventProcessor = new ReentrancySafeEventProcessor(HandleWindowCreation);
65 | _titleChangeEventProcessor = new ReentrancySafeEventProcessor(HandleWindowTitleChange);
66 | _windowDestructionEventProcessor = new ReentrancySafeEventProcessor(HandleWindowDestruction);
67 | }
68 |
69 | ///
70 | /// Activates the hook and starts looking for a running spotify process.
71 | ///
72 | /// The hook is already active.
73 | public override void Activate() {
74 | if (IsActive)
75 | throw new InvalidOperationException("Hook is already active.");
76 |
77 | Logger.Hook.LogDebug("Activated");
78 |
79 | IsActive = true;
80 |
81 | _windowCreationEventHook.HookGlobal();
82 | TryHookSpotify();
83 | }
84 |
85 | ///
86 | /// Deactivates the hook, clears hook data and stops looking for a running spotify process.
87 | ///
88 | /// The hook is not active.
89 | public override void Deactivate() {
90 | if (!IsActive)
91 | throw new InvalidOperationException("Hook has to be active.");
92 |
93 | IsActive = false;
94 |
95 | IsHooked = false;
96 | ClearHookData();
97 |
98 | Logger.Hook.LogDebug("Deactivated");
99 | }
100 |
101 | ///
102 | /// Try hooking to a currently running spotify process.
103 | ///
104 | /// A value indicating whether spotify could be hooked.
105 | protected bool TryHookSpotify() {
106 | var processes = FetchSpotifyProcesses(false);
107 |
108 | // find the main window process and handle
109 | Process? mainProcess = null;
110 | IntPtr mainWindowHandle = default;
111 | foreach (var process in processes) {
112 | var window = SpotifyUtils.GetMainSpotifyWindow(process);
113 | if (window is IntPtr handle) {
114 | mainProcess = process;
115 | mainWindowHandle = handle;
116 | break;
117 | }
118 | }
119 |
120 | if (mainProcess == null)
121 | return false;
122 |
123 | if (IsHooked)
124 | return true;
125 |
126 | OnSpotifyHooked(mainProcess, mainWindowHandle);
127 |
128 | return true;
129 | }
130 |
131 | private static bool IsWindowEvent(WinEventHookEventArgs eventArgs) {
132 | return eventArgs.ObjectId == AccessibleObjectID.OBJID_WINDOW && eventArgs.IsOwnEvent;
133 | }
134 |
135 | private void WindowCreationEventReceived(object sender, WinEventHookEventArgs e) {
136 | // ignore event if we are already hooked.
137 | if (IsHooked)
138 | return;
139 |
140 | // make sure that the created control is a window.
141 | if (!IsWindowEvent(e))
142 | return;
143 |
144 | // queue events and handle one after another
145 | // needed because this method gets called multiple times by the same thread at the same time (reentrant)
146 | _windowCreationEventProcessor.EnqueueAndProcess(e.WindowHandle);
147 | }
148 |
149 | [Lazy]
150 | private static Func GetProcessByIdFastFunc {
151 | get {
152 | // Expression Trees let us change a private field and are faster than reflection (if called multiple times)
153 | var processIdParameter = Expression.Parameter(typeof(uint), "processId");
154 | var processInfoType = typeof(Process).Assembly.GetType(typeof(Process).FullName + "Info");
155 | var constructor = typeof(Process).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, new Type[] { typeof(string), typeof(bool), typeof(int), processInfoType });
156 | var processIdConverted = Expression.Convert(processIdParameter, typeof(int));
157 | var newExpression = Expression.New(constructor, Expression.Constant("."), Expression.Constant(false), processIdConverted, Expression.Constant(null, processInfoType));
158 | var lambda = Expression.Lambda>(newExpression, processIdParameter);
159 | return lambda.Compile();
160 | }
161 | }
162 |
163 | private void HandleWindowCreation(IntPtr windowHandle) {
164 | // get created process
165 | var processId = NativeUtils.GetWindowThreadProcessId(windowHandle);
166 |
167 | // avoid semi costly validation checks
168 | var process = GetProcessByIdFastFunc(processId);
169 |
170 | // confirm that its a spotify process with a window.
171 | if (!SpotifyUtils.IsMainSpotifyWindow(windowHandle)) {
172 | process.Dispose();
173 | return;
174 | }
175 |
176 | OnSpotifyHooked(process, windowHandle);
177 |
178 | // ignore later events
179 | _windowCreationEventProcessor.FlushQueue();
180 | }
181 |
182 | private void WindowDestructionEventReceived(object sender, WinEventHookEventArgs e) {
183 | // ignore event if we are already unhooked.
184 | if (!IsHooked)
185 | return;
186 |
187 | // make sure that the destroyed control was a window.
188 | if (!IsWindowEvent(e))
189 | return;
190 |
191 | // make sure that the destroyed window was the main one.
192 | if (e.WindowHandle != MainWindowHandle)
193 | return;
194 |
195 | // queue events and handle one after another
196 | // needed because this method gets called multiple times by the same thread at the same time (reentrant)
197 | _windowDestructionEventProcessor.EnqueueAndProcess(e.WindowHandle);
198 | }
199 |
200 | private void HandleWindowDestruction(IntPtr windowHandle) {
201 | _windowDestructionEventProcessor.FlushQueue();
202 |
203 | if (MainWindowProcess == null)
204 | return;
205 |
206 | if (!MainWindowProcess.HasExited)
207 | return;
208 |
209 | UpdateSpotifyState(SpotifyState.ShuttingDown);
210 | OnSpotifyClosed();
211 | }
212 |
213 | private void TitleChangeEventReceived(object sender, WinEventHookEventArgs e) {
214 | // ignore event if we are not hooked.
215 | if (!IsHooked)
216 | return;
217 |
218 | // make sure that it was a window name that changed.
219 | if (!IsWindowEvent(e))
220 | return;
221 |
222 | // make sure that the source nmn window was the main one.
223 | if (e.WindowHandle != MainWindowHandle)
224 | return;
225 |
226 | // queue events and handle one after another
227 | // needed because this method gets called multiple times by the same thread at the same time (reentrant)
228 | _titleChangeEventProcessor.EnqueueAndProcess(e.WindowHandle);
229 | }
230 |
231 | private void HandleWindowTitleChange(IntPtr windowHandle) {
232 | UpdateWindowTitle(NativeUtils.GetWindowTitle(windowHandle));
233 | }
234 |
235 | ///
236 | /// OnSpotifyHooked is called whenever spotify is hooked.
237 | ///
238 | /// The main window spotify process.
239 | protected virtual void OnSpotifyHooked(Process mainProcess, IntPtr mainWindowHandle) {
240 | // ignore if already hooked
241 | if (IsHooked)
242 | return;
243 |
244 | MainWindowProcess = mainProcess;
245 | MainWindowHandle = mainWindowHandle;
246 |
247 | if (_windowCreationEventHook.Hooked)
248 | _windowCreationEventHook.Unhook();
249 |
250 | _titleChangeEventHook.HookToProcess(mainProcess);
251 | _windowDestructionEventHook.HookToProcess(mainProcess);
252 |
253 | // mainProcess.EnableRaisingEvents = true;
254 | // mainProcess.Exited += (s, e) => OnSpotifyClosed();
255 |
256 | IsHooked = true;
257 |
258 | UpdateWindowTitle(NativeUtils.GetWindowTitle(mainWindowHandle));
259 | }
260 |
261 | ///
262 | /// Fetch the spotify audio session.
263 | ///
264 | internal AudioSession? FetchAudioSession() {
265 | // Fetch sessions
266 | using var device = AudioDevice.GetDefaultAudioDevice(EDataFlow.eRender, ERole.eMultimedia);
267 | using var sessionManager = device.GetSessionManager();
268 | using var sessions = sessionManager.GetSessionCollection();
269 |
270 | // Check main process
271 | var sessionCount = sessions.Count;
272 | using var sessionCache = new DisposableList(sessionCount);
273 | for (var i = 0; i < sessions.Count; i++) {
274 | var session = sessions[i];
275 | if (session.ProcessID == MainWindowProcess?.Id) {
276 | Logger.Hook.LogInfo("Successfully fetched audio session using main window process.");
277 | return session;
278 | } else {
279 | // Store non-spotify sessions in disposable list to make sure that they the underlying COM objects are disposed.
280 | sessionCache.Add(session);
281 | }
282 | }
283 |
284 | Logger.Hook.LogWarning("Failed to fetch audio session using main window process.");
285 |
286 | // Try fetch through other "spotify" processes.
287 | var processes = FetchSpotifyProcesses();
288 |
289 | // Transfer the found sessions into a dictionary to speed up the search by process id.
290 | // (we do this here to avoid the overhead as most of the time we will find the session in the code above.)
291 | using var sessionMap = new ValueDisposableDictionary();
292 | foreach (var session in sessionCache)
293 | sessionMap.Add(session.ProcessID, session);
294 | sessionCache.Clear();
295 |
296 | foreach (var process in processes) {
297 | var processId = (uint)process.Id;
298 |
299 | // skip main process as we already checked it
300 | if (MainWindowProcess?.Id == processId)
301 | continue;
302 |
303 | if (sessionMap.TryGetValue(processId, out AudioSession session)) {
304 | Logger.Hook.LogInfo("Successfully fetched audio session using secondary spotify processes.");
305 |
306 | // remove from map to avoid disposal
307 | sessionMap.Remove(processId);
308 | return _audioSession;
309 | }
310 | }
311 |
312 | Logger.Hook.LogError("Failed to fetch audio session.");
313 |
314 | return null;
315 | }
316 |
317 | ///
318 | /// A cache for all running spotify processes.
319 | ///
320 | private Process[]? _processesCache;
321 |
322 | ///
323 | /// Fetches all running processes with the name "spotify". This method uses caching, but makes sure that the returned processes are running.
324 | ///
325 | /// An array of running spotify processes.
326 | internal Process[] FetchSpotifyProcesses(bool useCache = true) {
327 | if (!useCache || _processesCache?.Any(process => !process.IsAssociated() || process.HasExited) != false)
328 | return _processesCache = SpotifyUtils.GetSpotifyProcesses().ToArray();
329 |
330 | return _processesCache;
331 | }
332 |
333 | ///
334 | /// Update the current window title to a new value.
335 | ///
336 | /// The new spotify window title.
337 | protected void UpdateWindowTitle(string newWindowTitle) {
338 | if (newWindowTitle == WindowTitle)
339 | return;
340 |
341 | var oldWindowTitle = WindowTitle;
342 | WindowTitle = newWindowTitle;
343 | OnWindowTitleChanged(oldWindowTitle, newWindowTitle);
344 | }
345 |
346 | ///
347 | /// OnWindowTitleChanged is called whenever the main spotify window changes its title.
348 | ///
349 | /// The old window title.
350 | /// The new window title.
351 | protected virtual void OnWindowTitleChanged(string? oldWindowTitle, string newWindowTitle) {
352 | Logger.Hook.LogDebug($"Current window name changed to \"{newWindowTitle}\"");
353 | switch (newWindowTitle) {
354 | // Paused / Default for Free version
355 | case "Spotify Free":
356 | // Paused / Default for Premium version
357 | // Why do you need EZBlocker3 when you have premium?
358 | case "Spotify Premium":
359 | UpdateSpotifyState(SpotifyState.Paused);
360 | break;
361 | // Advertisment Playing
362 | case "Advertisement":
363 | UpdateSpotifyState(SpotifyState.PlayingAdvertisement);
364 | break;
365 | // Advertisment playing or Starting up
366 | case "Spotify":
367 | if (oldWindowTitle?.Length == 0) {
368 | UpdateSpotifyState(SpotifyState.StartingUp);
369 | } else if (oldWindowTitle == null) {
370 | if (MainWindowProcess is null)
371 | throw new IllegalStateException();
372 |
373 | // check how long spotify has been running
374 | if ((DateTime.Now - MainWindowProcess.StartTime) < TimeSpan.FromMilliseconds(3000)) {
375 | UpdateSpotifyState(SpotifyState.StartingUp);
376 | } else {
377 | UpdateSpotifyState(SpotifyState.PlayingAdvertisement);
378 | }
379 | } else {
380 | UpdateSpotifyState(SpotifyState.PlayingAdvertisement);
381 | }
382 | break;
383 | // Shutting down
384 | case "":
385 | if (oldWindowTitle is null)
386 | UpdateSpotifyState(SpotifyState.StartingUp);
387 | else
388 | UpdateSpotifyState(SpotifyState.ShuttingDown);
389 | break;
390 | // Song Playing: "[artist] - [title]"
391 | case var name when name?.Contains(" - ") == true:
392 | var (artist, title) = name.Split(" - ", maxCount: 2).Select(e => e.Trim()).ToArray();
393 | UpdateSpotifyState(SpotifyState.PlayingSong, newSong: new SongInfo(title, artist));
394 | break;
395 | // What is happening?
396 | default:
397 | Logger.Hook.LogWarning($"Spotify entered an unknown state. (WindowTitle={newWindowTitle})");
398 | if (AssumeAdOnUnknownState) {
399 | Logger.Hook.LogInfo($"Assuming WindowTitle={newWindowTitle} marks an ad.");
400 | UpdateSpotifyState(SpotifyState.PlayingAdvertisement);
401 | } else {
402 | UpdateSpotifyState(SpotifyState.Unknown);
403 | }
404 | break;
405 | }
406 | }
407 |
408 | ///
409 | /// OnSpotifyClosed is called whenever spotify is unhooked.
410 | ///
411 | protected virtual void OnSpotifyClosed() {
412 | Logger.Hook.LogWarning("Spotify closed.");
413 |
414 | ClearHookData();
415 |
416 | IsHooked = false;
417 |
418 | _windowCreationEventHook.HookGlobal();
419 |
420 | // scan for spotify to make sure it did not start again while we were shutting down. (should happen only during debugging)
421 | TryHookSpotify();
422 | }
423 |
424 | ///
425 | /// Clears all the state associated with a hooked spotify process.
426 | ///
427 | protected void ClearHookData() {
428 | MainWindowProcess?.Dispose();
429 | _processesCache?.DisposeAll();
430 | _audioSession?.Dispose();
431 |
432 | MainWindowProcess = null;
433 | MainWindowHandle = null;
434 | _processesCache = null;
435 | _audioSession = null;
436 | WindowTitle = null;
437 | IsHooked = false;
438 |
439 | _windowCreationEventHook.TryUnhook();
440 | _titleChangeEventHook.TryUnhook();
441 | _windowDestructionEventHook.TryUnhook();
442 | }
443 |
444 | ///
445 | public bool SetMute(bool mute) {
446 | if (!IsHooked)
447 | return false;
448 |
449 | // ensure audio session
450 | if (AudioSession is null) {
451 | Logger.Hook.LogError($"Failed to {(mute ? "mute" : "unmute")} spotify due to missing audio session.");
452 | return false;
453 | }
454 |
455 | // mute
456 | try {
457 | AudioSession.IsMuted = mute;
458 | Logger.Hook.LogInfo($"Spotify {(mute ? "muted" : "unmuted")}.");
459 | return true;
460 | } catch (Exception e) {
461 | Logger.Hook.LogException($"Failed to {(mute ? "mute" : "unmute")} spotify:", e);
462 | return false;
463 | }
464 | }
465 |
466 | protected override void Dispose(bool disposing) {
467 | base.Dispose(disposing);
468 |
469 | if (!disposing)
470 | return;
471 |
472 | MainWindowProcess?.Dispose();
473 | _processesCache?.DisposeAll();
474 |
475 | AudioSession?.Dispose();
476 |
477 | _windowCreationEventHook?.Dispose();
478 | _titleChangeEventHook?.Dispose();
479 | _windowDestructionEventHook?.Dispose();
480 | }
481 | }
482 | }
483 |
--------------------------------------------------------------------------------
/EZBlocker3/Spotify/SkippingSpotifyAdBlocker.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using EZBlocker3.Extensions;
6 | using EZBlocker3.Interop;
7 | using EZBlocker3.Logging;
8 | using EZBlocker3.Settings;
9 | using Microsoft.Windows.Sdk;
10 |
11 | namespace EZBlocker3.Spotify {
12 | public class SkippingSpotifyAdBlocker : AbstractSpotifyAdBlocker {
13 | private SongInfo? lastActiveSong;
14 |
15 | public SkippingSpotifyAdBlocker(ISpotifyHook hook) : base(hook) { }
16 |
17 | protected override void OnSpotifyStateChanged(object sender, SpotifyStateChangedEventArgs e) {
18 | base.OnSpotifyStateChanged(sender, e);
19 |
20 | if (e.NewState == SpotifyState.PlayingAdvertisement) {
21 | Logger.AdSkipper.LogInfo("Starting to skip ad");
22 | Task.Run(() => KillAndRestartSpotifyAsync());
23 | }
24 | }
25 | protected override void OnActiveSongChanged(object sender, ActiveSongChangedEventArgs e) {
26 | base.OnActiveSongChanged(sender, e);
27 |
28 | if (e.NewActiveSong != null)
29 | lastActiveSong = e.NewActiveSong;
30 | }
31 |
32 | private async Task KillAndRestartSpotifyAsync() {
33 | Logger.AdSkipper.LogInfo("Killing spotify...");
34 | await KillSpotifyAsync().ConfigureAwait(false);
35 | Logger.AdSkipper.LogInfo("Restarting spotify...");
36 | RestartSpotify();
37 | }
38 |
39 | private static async Task KillSpotifyAsync() {
40 | var processes = SpotifyUtils.GetSpotifyProcesses().ToArray();
41 |
42 | // Simulate closing the main window
43 | var mainWindowHandle = (HWND?)processes.Select(process => SpotifyUtils.GetMainSpotifyWindow(process)).FirstOrDefault(e => e != null);
44 | if (mainWindowHandle is HWND hwnd) {
45 | PInvoke.SendMessage(hwnd, Constants.WM_APPCOMMAND, default, (LPARAM)(IntPtr)SpotifyAppCommands.PlayPause);
46 | PInvoke.SendMessage(hwnd, Constants.WM_ERASEBKGND, default, default);
47 | PInvoke.SendMessage(hwnd, Constants.WM_WINDOWPOSCHANGING, default, default);
48 | PInvoke.SendMessage(hwnd, 0x90 /* WM_ACCESS_WINDOW */, default, default);
49 | PInvoke.SendMessage(hwnd, 0x272 /* WM_UNREGISTER_WINDOW_SERVICES */, default, default);
50 | PInvoke.SendMessage(hwnd, Constants.WM_DESTROY, default, default);
51 | PInvoke.SendMessage(hwnd, Constants.WM_NCDESTROY, default, default);
52 | } else {
53 | Logger.AdSkipper.LogInfo("Failed to find main spotify window handle");
54 | }
55 |
56 | // we need to wait for all processes to exit or otherwise spotify will show an error when we try to start it again.
57 | using var cancellationSource = new CancellationTokenSource();
58 | var cancellationToken = cancellationSource.Token;
59 | var timeoutTask = Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
60 | var processesExitedTask = Task.WhenAll(processes.Select(p => p.WaitForExitAsync(cancellationToken)));
61 | if (mainWindowHandle is null || await Task.WhenAny(timeoutTask, processesExitedTask).ConfigureAwait(false) == timeoutTask) {
62 | Logger.AdSkipper.LogInfo("Spotify did not shut down - kill it with fire!");
63 | // some processes did not shut down in time -> kill them
64 | foreach (var process in processes) {
65 | if (!process.HasExited)
66 | process.Kill();
67 | }
68 | }
69 | // ensure all tasks finish
70 | cancellationSource.Cancel();
71 | }
72 |
73 | private void RestartSpotify() {
74 | Hook.SpotifyStateChanged += Handler1;
75 |
76 | StartWithSpotify.StartSpotify(ignoreProxy: true);
77 |
78 | // handler to catch spotify after restart
79 | void Handler1(object sender, SpotifyStateChangedEventArgs _) {
80 | if (Hook.State == SpotifyState.Paused) {
81 | Hook.SpotifyStateChanged -= Handler1;
82 |
83 | var process = SpotifyUtils.GetMainSpotifyProcess();
84 | var windowHandle = NativeUtils.GetMainWindowOfProcess(process!);
85 |
86 | Task.Run(async () => {
87 | await Task.Delay(1000).ConfigureAwait(false); // if we do not wait here spotify won't update the window title and we won't detect a state change.
88 |
89 | // handler to catch spotify after playback resumes
90 | void Handler2(object sender, SpotifyStateChangedEventArgs _) {
91 | if (Hook.State == SpotifyState.PlayingSong && windowHandle != IntPtr.Zero) {
92 | Hook.SpotifyStateChanged -= Handler2;
93 | if (Hook.ActiveSong is SongInfo current && lastActiveSong is SongInfo previous && current == previous) {
94 | Logger.AdSkipper.LogInfo("Previous song was resumed - skipping to next track");
95 | PInvoke.SendMessage((HWND)windowHandle, Constants.WM_APPCOMMAND, default, (LPARAM)(IntPtr)SpotifyAppCommands.NextTrack);
96 | }
97 | }
98 | }
99 | Hook.SpotifyStateChanged += Handler2;
100 |
101 | Logger.AdSkipper.LogInfo("Resuming playback...");
102 | PInvoke.SendMessage((HWND)windowHandle, Constants.WM_APPCOMMAND, default, (LPARAM)(IntPtr)SpotifyAppCommands.PlayPause);
103 | });
104 | }
105 | }
106 | }
107 |
108 | public enum SpotifyAppCommands : int {
109 | // Mute = 0x80000, mutes everything?
110 | VolumeDown = 0x90000,
111 | VolumeUp = 0xA0000,
112 | NextTrack = 0xB0000,
113 | PreviousTrack = 0xC0000,
114 | // Stop = 0xD0000, does not seam to work
115 | PlayPause = 0xE0000
116 | }
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/EZBlocker3/Spotify/SongInfo.cs:
--------------------------------------------------------------------------------
1 | namespace EZBlocker3.Spotify {
2 | public record SongInfo(string Title, string Artist) {
3 |
4 | public override string ToString() => $"{Title} by {Artist}";
5 |
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/EZBlocker3/Spotify/SpotifyHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Microsoft.Windows.Sdk;
5 |
6 | namespace EZBlocker3.Spotify {
7 | public sealed class SpotifyHandler : IActivatable, IDisposable {
8 | public ISpotifyHook Hook { get; }
9 | public IMutingSpotifyHook? Muter { get; }
10 | public IActivatable? AdBlocker { get; }
11 | public bool IsActive { get; private set; }
12 | private readonly CancellationTokenSource cancellationTokenSource = new();
13 |
14 | public SpotifyHandler() {
15 | switch (Properties.Settings.Default.Hook) {
16 | case nameof(GlobalSystemMediaTransportControlSpotifyHook):
17 | Hook = new GlobalSystemMediaTransportControlSpotifyHook();
18 | break;
19 | // case nameof(ProcessAndWindowEventSpotifyHook):
20 | default:
21 | var hook = new ProcessAndWindowEventSpotifyHook() {
22 | AssumeAdOnUnknownState = Properties.Settings.Default.AggressiveMuting
23 | };
24 | Hook = hook;
25 | Muter = hook;
26 | break;
27 | }
28 |
29 | AdBlocker = (Properties.Settings.Default.BlockType, Hook) switch {
30 | (nameof(MutingSpotifyAdBlocker), IMutingSpotifyHook muter) => new MutingSpotifyAdBlocker(Hook, muter) {
31 | AggressiveMuting = Properties.Settings.Default.AggressiveMuting
32 | },
33 | // (nameof(SkippingSpotifyAdBlocker), _) =>
34 | _ => new SkippingSpotifyAdBlocker(Hook),
35 | };
36 | }
37 |
38 | public void Activate() {
39 | Task.Run(() => {
40 | AdBlocker?.Activate();
41 | Hook.Activate();
42 |
43 | while (!cancellationTokenSource.IsCancellationRequested) {
44 | var res = PInvoke.GetMessage(out var msg, default, 0, 0);
45 |
46 | if (!res)
47 | break;
48 |
49 | PInvoke.TranslateMessage(msg);
50 | PInvoke.DispatchMessage(msg);
51 | }
52 | }, cancellationTokenSource.Token);
53 |
54 | IsActive = true;
55 | }
56 |
57 | public void Deactivate() {
58 | AdBlocker?.Deactivate();
59 | Hook.Deactivate();
60 |
61 | IsActive = false;
62 | }
63 |
64 | public void Dispose() {
65 | Hook.Dispose();
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/EZBlocker3/Spotify/SpotifyState.cs:
--------------------------------------------------------------------------------
1 | namespace EZBlocker3.Spotify {
2 | ///
3 | /// Represents the current state of a running spotify process.
4 | ///
5 | public enum SpotifyState {
6 | ///
7 | /// Spotify is in an unknown state.
8 | ///
9 | Unknown,
10 | ///
11 | /// Spotify is playing a song.
12 | ///
13 | PlayingSong,
14 | ///
15 | /// Spotify is playing an advertisement.
16 | ///
17 | PlayingAdvertisement,
18 | ///
19 | /// Spotify is paused.
20 | ///
21 | Paused,
22 | ///
23 | /// Spotify is in the process of starting up.
24 | ///
25 | StartingUp,
26 | ///
27 | /// Spotify is in the process of shutting down.
28 | ///
29 | ShuttingDown
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/EZBlocker3/Spotify/SpotifyUtils.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using EZBlocker3.Interop;
6 |
7 | namespace EZBlocker3.Spotify {
8 | public static class SpotifyUtils {
9 | public static IEnumerable GetSpotifyProcesses() {
10 | return Process.GetProcesses().Where(p => IsSpotifyProcess(p));
11 | }
12 |
13 | public static Process? GetMainSpotifyProcess() {
14 | return Array.Find(Process.GetProcesses(), p => IsMainSpotifyProcess(p));
15 | }
16 |
17 | public static bool IsSpotifyProcess(Process? process) {
18 | if (process is null)
19 | return false;
20 |
21 | if (!process.ProcessName.StartsWith("spotify", StringComparison.OrdinalIgnoreCase))
22 | return false;
23 |
24 | return true;
25 | }
26 |
27 | public static bool IsMainSpotifyProcess(Process? process) {
28 | if (!IsSpotifyProcess(process))
29 | return false;
30 |
31 | return NativeUtils.GetAllWindowsOfProcess(process!).Any(hwnd => IsMainSpotifyWindow(hwnd));
32 | }
33 |
34 | public static bool IsMainSpotifyWindow(IntPtr windowHandle) {
35 | var windowTitle = NativeUtils.GetWindowTitle(windowHandle);
36 |
37 | if (string.IsNullOrWhiteSpace(windowTitle))
38 | return false;
39 |
40 | if (windowTitle == "G" || windowTitle == "Default IME")
41 | return false;
42 |
43 | var windowClassName = NativeUtils.GetWindowClassName(windowHandle);
44 |
45 | return windowClassName.Equals("Chrome_WidgetWin_0", StringComparison.Ordinal);
46 | }
47 |
48 | public static IntPtr? GetMainSpotifyWindow(Process process) {
49 | var window = NativeUtils.GetAllWindowsOfProcess(process).Find(window => IsMainSpotifyWindow(window));
50 | return window == default ? null : window;
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/EZBlocker3/Utils/BitmapUtils.cs:
--------------------------------------------------------------------------------
1 | using System.Drawing;
2 | using System.Drawing.Imaging;
3 | using System.IO;
4 |
5 | namespace EZBlocker3.Utils {
6 | internal static class BitmapUtils {
7 | // based on https://stackoverflow.com/a/11448060/6304917
8 | public static void SaveAsIcon(Bitmap sourceBitmap, string filePath) {
9 | using var file = new FileStream(filePath, FileMode.Create);
10 | SaveAsIcon(sourceBitmap, file);
11 | }
12 |
13 | public static void SaveAsIcon(Bitmap sourceBitmap, Stream stream) {
14 | // ICO header
15 | stream.WriteByte(0); stream.WriteByte(0);
16 | stream.WriteByte(1); stream.WriteByte(0);
17 | stream.WriteByte(1); stream.WriteByte(0);
18 |
19 | // Image size
20 | stream.WriteByte((byte)sourceBitmap.Width);
21 | stream.WriteByte((byte)sourceBitmap.Height);
22 | // Palette
23 | stream.WriteByte(0);
24 | // Reserved
25 | stream.WriteByte(0);
26 | // Number of color planes
27 | stream.WriteByte(0); stream.WriteByte(0);
28 | // Bits per pixel
29 | stream.WriteByte(32); stream.WriteByte(0);
30 |
31 | // Data size, will be written after the data
32 | stream.WriteByte(0);
33 | stream.WriteByte(0);
34 | stream.WriteByte(0);
35 | stream.WriteByte(0);
36 |
37 | // Offset to image data, fixed at 22
38 | stream.WriteByte(22);
39 | stream.WriteByte(0);
40 | stream.WriteByte(0);
41 | stream.WriteByte(0);
42 |
43 | // Writing actual data
44 | sourceBitmap.Save(stream, ImageFormat.Png);
45 |
46 | // Getting data length (file length minus header)
47 | var len = stream.Length - 22;
48 |
49 | // Write it in the correct place
50 | stream.Seek(14, SeekOrigin.Begin);
51 | stream.WriteByte((byte)len);
52 | stream.WriteByte((byte)(len >> 8));
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/EZBlocker3/Utils/DisposableList.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace EZBlocker3.Utils {
5 | internal sealed class DisposableList : List, IDisposable where T : IDisposable? {
6 | public DisposableList() { }
7 | public DisposableList(int capacity) : base(capacity) { }
8 | public DisposableList(IEnumerable collection) : base(collection) { }
9 |
10 | public void Dispose() {
11 | foreach (var obj in this) {
12 | obj?.Dispose();
13 | }
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/EZBlocker3/Utils/KeyDisposableDictionary.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace EZBlocker3.Utils {
5 | internal sealed class KeyDisposableDictionary : Dictionary, IDisposable
6 | where TKey : IDisposable? {
7 | public KeyDisposableDictionary() { }
8 | public KeyDisposableDictionary(int capacity) : base(capacity) { }
9 | public KeyDisposableDictionary(IEqualityComparer comparer) : base(comparer) { }
10 | public KeyDisposableDictionary(IDictionary dictionary) : base(dictionary) { }
11 | public KeyDisposableDictionary(int capacity, IEqualityComparer comparer) : base(capacity, comparer) { }
12 | public KeyDisposableDictionary(IDictionary dictionary, IEqualityComparer comparer) : base(dictionary, comparer) { }
13 | // protected KeyDisposableDictionary(SerializationInfo info, StreamingContext context) : base(info, context) { }
14 |
15 | public void Dispose() {
16 | foreach (var key in Keys) {
17 | key?.Dispose();
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/EZBlocker3/Utils/KeyValueDisposableDictionary.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using EZBlocker3.Extensions;
4 |
5 | namespace EZBlocker3.Utils {
6 | internal sealed class KeyValueDisposableDictionary : Dictionary, IDisposable
7 | where TKey : IDisposable?
8 | where TValue : IDisposable? {
9 | public KeyValueDisposableDictionary() { }
10 | public KeyValueDisposableDictionary(int capacity) : base(capacity) { }
11 | public KeyValueDisposableDictionary(IEqualityComparer comparer) : base(comparer) { }
12 | public KeyValueDisposableDictionary(IDictionary dictionary) : base(dictionary) { }
13 | public KeyValueDisposableDictionary(int capacity, IEqualityComparer comparer) : base(capacity, comparer) { }
14 | public KeyValueDisposableDictionary(IDictionary dictionary, IEqualityComparer comparer) : base(dictionary, comparer) { }
15 | // protected FullDisposableDictionary(SerializationInfo info, StreamingContext context) : base(info, context) { }
16 |
17 | public void Dispose() {
18 | foreach (var (key, value) in this) {
19 | key?.Dispose();
20 | value?.Dispose();
21 | }
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/EZBlocker3/Utils/ValueDisposableDictionary.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace EZBlocker3.Utils {
5 | internal sealed class ValueDisposableDictionary : Dictionary, IDisposable
6 | where TValue : IDisposable? {
7 | public ValueDisposableDictionary() { }
8 | public ValueDisposableDictionary(int capacity) : base(capacity) { }
9 | public ValueDisposableDictionary(IEqualityComparer comparer) : base(comparer) { }
10 | public ValueDisposableDictionary(IDictionary dictionary) : base(dictionary) { }
11 | public ValueDisposableDictionary(int capacity, IEqualityComparer comparer) : base(capacity, comparer) { }
12 | public ValueDisposableDictionary(IDictionary dictionary, IEqualityComparer comparer) : base(dictionary, comparer) { }
13 | // protected ValueDisposableDictionary(SerializationInfo info, StreamingContext context) : base(info, context) { }
14 |
15 | public void Dispose() {
16 | foreach (var value in Values) {
17 | value?.Dispose();
18 | }
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/EZBlocker3/Utils/WindowHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Windows;
3 |
4 | namespace EZBlocker3.Utils {
5 | internal static class WindowHelper {
6 | public static void ApplySizeToContentFix(Window window) {
7 | void Handler(object sender, EventArgs eventArgs) {
8 | window.InvalidateMeasure();
9 | window.SourceInitialized -= Handler;
10 | }
11 |
12 | window.SourceInitialized += Handler;
13 | }
14 |
15 | public static readonly DependencyProperty ApplySizeToContentFixProperty =
16 | DependencyProperty.RegisterAttached(
17 | "ApplySizeToContentFix",
18 | typeof(bool),
19 | typeof(WindowHelper),
20 | new PropertyMetadata(OnApplySizeToContentFixChanged));
21 |
22 | public static bool GetApplySizeToContentFix(Window window) {
23 | return (bool)window.GetValue(ApplySizeToContentFixProperty);
24 | }
25 |
26 | public static void SetApplySizeToContentFix(Window window, bool value) {
27 | window.SetValue(ApplySizeToContentFixProperty, value);
28 | }
29 |
30 | private static void OnApplySizeToContentFixChanged(DependencyObject obj, DependencyPropertyChangedEventArgs eventArgs) {
31 | bool newValue = (bool)eventArgs.NewValue;
32 |
33 | if (newValue && obj is Window window)
34 | ApplySizeToContentFix(window);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/EZBlocker3/app.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
52 |
59 |
60 |
61 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/Interop/Interop.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net472
5 | 9.0
6 |
7 |
8 |
9 |
10 | all
11 | runtime; build; native; contentfiles; analyzers; buildtransitive
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/Interop/NativeMethods.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/microsoft/CsWin32/main/src/Microsoft.Windows.CsWin32/settings.schema.json",
3 | "public": true
4 | }
--------------------------------------------------------------------------------
/Interop/NativeMethods.txt:
--------------------------------------------------------------------------------
1 | // Win32
2 | SendMessage
3 | WM_CLOSE
4 | WM_QUIT
5 | WM_DESTROY
6 | WM_NCDESTROY
7 | WM_APPCOMMAND
8 | WM_SYSCOMMAND
9 | WM_WINDOWPOSCHANGING
10 | WM_ERASEBKGND
11 | SC_CLOSE
12 | EnumWindows
13 | GetWindowText
14 | GetWindowTextLength
15 | GetWindowThreadProcessId
16 | GetWindow
17 | WINDOWS_STYLE
18 | GetWindow_uCmdFlags
19 | GetWindowLongPtr_nIndex
20 | CoCreateInstance
21 | CLSCTX
22 | EnumThreadWindows
23 | TranslateMessage
24 | DispatchMessage
25 | GetMessage
26 | CloseWindow
27 | GetClassName
28 |
29 | // COM
30 | MMDeviceEnumerator
31 | IMMDeviceEnumerator
32 | ISimpleAudioVolume
33 | IMMDevice
34 | IAudioSessionManager2
35 | IAudioSessionEnumerator
36 | IAudioSessionControl2
37 | IAudioSessionControl
38 | IAudioMeterInformation
39 | ERole
40 | EDataFlow
41 | S_OK
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # EZBlocker 3
2 |
3 | EZBlocker 3 is a Spotify Ad Muter/Blocker for Windows written in C#.
4 | It mutes Spotify while an advertisement is playing.
5 | It is a complete rewrite of the original [EZBlocker](https://github.com/Xeroday/Spotify-Ad-Blocker), as it did not mute all the ads and Spotify was always muted on startup.
6 |
7 | ## Features
8 | Comparing to the original, EZBlocker 3...
9 | - ... provides better muting (e.g. [#215](https://github.com/Xeroday/Spotify-Ad-Blocker/pull/215))
10 | - ... does not include any sort of analytics ([#103](https://github.com/Xeroday/Spotify-Ad-Blocker/issues/103)).
11 | - ... is built using a [modern UI](https://github.com/Kinnara/ModernWpf)
12 | - ... offers automatic updating
13 | - ... uses an event based approach instead of polling for a smaller performance footprint
14 |
15 | 
16 | 
17 |
18 | ## Setup
19 |
20 | **[Download for Windows](https://github.com/OpenByteDev/EZBlocker3/releases/latest/)**
21 |
22 | No need to install as EZBlocker 3 is a portable application.
23 |
24 | ## Remove application
25 |
26 | To remove EZBlocker 3 the "Uninstall" button in the settings menu should be used.
27 |
28 | ## Technical overview
29 |
30 | ### UI
31 | The EZBlocker 3 UI ist built using the [Windows Presentation Foundation](https://docs.microsoft.com/en-us/dotnet/desktop/wpf/introduction-to-wpf?view=netframeworkdesktop-4.8) and the [ModernWPF UI Library](https://github.com/Kinnara/ModernWpf) for a modern Windows 10 look and system theme awareness.
32 |
33 | ### Ad detection
34 | Ads are detected by checking the spotify window title. On startup EZBlocker scans for running processes named "spotify" that have a window and extracts the title. It then listens for title changes by handling the `EVENT_OBJECT_NAMECHANGE` event using [`SetWinEventHook`](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwineventhook).
35 | Spotify startup and shutdown are detected by handling the `EVENT_OBJECT_DESTROY`and `EVENT_OBJECT_SHOW` events.
36 |
37 | ### Auto update
38 | EZBlocker 3 checks for new releases on startup using the [Github REST API](https://docs.github.com/en/free-pro-team@latest/rest). If a newer version is found, it is downloaded next to the app. The `EZBlocker3.exe` file is then switched out while it is still running and then restarted.
39 |
40 | ## Credits
41 |
42 | - [Eric Zhang](https://github.com/Xeroday) for the original [EZBlocker](https://github.com/Xeroday/Spotify-Ad-Blocker)
43 | - [Yimeng Wu](https://github.com/Kinnara) for the beautiful [ModernWPF UI Library](https://github.com/Kinnara/ModernWpf)
44 | - [James Newton-King](https://github.com/JamesNK) for his omnipresent [Json.NET](https://www.newtonsoft.com/json)
45 |
--------------------------------------------------------------------------------
/screenshots/screenshot-app-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenByteDev/EZBlocker3/3e868aaa54b154329cba25f46eaf18ac9eb1ffcb/screenshots/screenshot-app-dark.png
--------------------------------------------------------------------------------
/screenshots/screenshot-app-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenByteDev/EZBlocker3/3e868aaa54b154329cba25f46eaf18ac9eb1ffcb/screenshots/screenshot-app-light.png
--------------------------------------------------------------------------------