diff --git a/Snip/Globals.cs b/Snip/Globals.cs
index 0e3b3ae..082d0c2 100644
--- a/Snip/Globals.cs
+++ b/Snip/Globals.cs
@@ -90,7 +90,8 @@ public enum MediaPlayerSelection : int
{
NoPlayer = 0,
Spotify = 1,
- Itunes = 2
+ Itunes = 2,
+ YouTubeMusic = 3
}
public enum MediaCommand : int
diff --git a/Snip/LocalizedMessages.cs b/Snip/LocalizedMessages.cs
index 6c5a3ba..8bc2e3c 100644
--- a/Snip/LocalizedMessages.cs
+++ b/Snip/LocalizedMessages.cs
@@ -27,6 +27,7 @@ public static class LocalizedMessages
public static string NoPlayer { get; set; }
public static string Spotify { get; set; }
public static string Itunes { get; set; }
+ public static string YouTubeMusic { get; set; }
public static string SwitchedToPlayer { get; set; }
public static string PlayerIsNotRunning { get; set; }
public static string NoTrackPlaying { get; set; }
diff --git a/Snip/Players/YouTubeMusic.cs b/Snip/Players/YouTubeMusic.cs
new file mode 100644
index 0000000..1b6be6a
--- /dev/null
+++ b/Snip/Players/YouTubeMusic.cs
@@ -0,0 +1,624 @@
+#region File Information
+/*
+ * Copyright (C) 2012-2026 David Rudie
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111, USA.
+ */
+#endregion
+
+namespace Winter
+{
+ using System;
+ using System.ComponentModel;
+ using System.Globalization;
+ using System.IO;
+ using System.Net;
+ using System.Text;
+ using System.Timers;
+ using System.Windows.Forms;
+ using SimpleJson;
+
+ using Timer = System.Timers.Timer;
+
+ internal sealed class YouTubeMusic : MediaPlayer, IDisposable
+ {
+ #region Fields
+
+ // ytmdesktop companion server defaults
+ private string companionServerAddress = "http://127.0.0.1:9863";
+
+ private string authorizationToken = string.Empty;
+
+ private Timer updateTrackInformation;
+ private double updateTrackInformationDefaultInterval = 3000;
+
+ // Auth flow state
+ private bool isAuthorizing = false;
+ private bool authorizationFailed = false;
+
+ private string appId = "snip_media_player";
+ private string appName = "Snip";
+ private string appVersion = "1.0.0";
+
+ #endregion
+
+ #region Methods
+
+ public override void Load()
+ {
+ base.Load();
+
+ // Try to load saved token from registry
+ this.LoadAuthToken();
+
+ if (string.IsNullOrEmpty(this.authorizationToken))
+ {
+ // No saved token; we'll attempt authorization on first update
+ this.authorizationFailed = false;
+ }
+
+ this.updateTrackInformation = new Timer(this.updateTrackInformationDefaultInterval);
+ this.updateTrackInformation.Elapsed += this.UpdateTrackInformation_Elapsed;
+ this.updateTrackInformation.AutoReset = true;
+ this.updateTrackInformation.Enabled = true;
+ }
+
+ public override void Unload()
+ {
+ base.Unload();
+
+ if (this.updateTrackInformation != null)
+ {
+ this.updateTrackInformation.Stop();
+ }
+ }
+
+ public void Dispose()
+ {
+ if (this.updateTrackInformation != null)
+ {
+ this.updateTrackInformation.Dispose();
+ this.updateTrackInformation = null;
+ }
+ }
+
+ private void LoadAuthToken()
+ {
+ try
+ {
+ Microsoft.Win32.RegistryKey registryKey = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "SOFTWARE\\{0}\\{1}",
+ AssemblyInformation.AssemblyTitle,
+ System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.Major));
+
+ if (registryKey != null)
+ {
+ string savedToken = Convert.ToString(registryKey.GetValue("YouTubeMusic Auth Token", string.Empty), CultureInfo.CurrentCulture);
+ registryKey.Close();
+
+ if (!string.IsNullOrEmpty(savedToken))
+ {
+ this.authorizationToken = savedToken;
+ }
+ }
+ }
+ catch
+ {
+ // Silently fail; we'll try to get a new token
+ }
+ }
+
+ private void SaveAuthToken()
+ {
+ try
+ {
+ Microsoft.Win32.RegistryKey registryKey = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "SOFTWARE\\{0}\\{1}",
+ AssemblyInformation.AssemblyTitle,
+ System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.Major));
+
+ registryKey.SetValue("YouTubeMusic Auth Token", this.authorizationToken);
+ registryKey.Close();
+ }
+ catch
+ {
+ // Silently fail
+ }
+ }
+
+ private bool TryAuthorize()
+ {
+ if (this.isAuthorizing)
+ {
+ return false;
+ }
+
+ this.isAuthorizing = true;
+
+ try
+ {
+ // Step 1: Request a code
+ string requestCodeJson = this.PostJson(
+ this.companionServerAddress + "/api/v1/auth/requestcode",
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{{\"appId\":\"{0}\",\"appName\":\"{1}\",\"appVersion\":\"{2}\"}}",
+ this.appId,
+ this.appName,
+ this.appVersion),
+ false);
+
+ if (string.IsNullOrEmpty(requestCodeJson))
+ {
+ this.isAuthorizing = false;
+ return false;
+ }
+
+ dynamic codeResponse = SimpleJson.DeserializeObject(requestCodeJson);
+ string code = codeResponse.code.ToString();
+
+ // Show the code to the user so they can confirm in ytmdesktop
+ Globals.SnipNotifyIcon.ShowBalloonTip(
+ 30000,
+ "Snip - YouTube Music Authorization",
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "Please check YouTube Music Desktop App and confirm the authorization.\nCode: {0}",
+ code),
+ ToolTipIcon.Info);
+
+ // Step 2: Request the auth token (long timeout — blocks until user approves in ytmdesktop)
+ string requestTokenJson = this.PostJson(
+ this.companionServerAddress + "/api/v1/auth/request",
+ string.Format(
+ CultureInfo.InvariantCulture,
+ "{{\"appId\":\"{0}\",\"code\":\"{1}\"}}",
+ this.appId,
+ code),
+ false,
+ 60);
+
+ if (!string.IsNullOrEmpty(requestTokenJson))
+ {
+ dynamic tokenResponse = SimpleJson.DeserializeObject(requestTokenJson);
+ this.authorizationToken = tokenResponse.token.ToString();
+ this.SaveAuthToken();
+
+ Globals.SnipNotifyIcon.ShowBalloonTip(
+ 5000,
+ "Snip",
+ "Successfully authorized with YouTube Music Desktop App!",
+ ToolTipIcon.Info);
+
+ this.isAuthorizing = false;
+ this.authorizationFailed = false;
+ return true;
+ }
+ }
+ catch (WebException)
+ {
+ // Authorization denied, timed out, or ytmdesktop not running
+ }
+ catch
+ {
+ // Other errors
+ }
+
+ this.isAuthorizing = false;
+ this.authorizationFailed = true;
+ return false;
+ }
+
+ private void UpdateTrackInformation_Elapsed(object sender, ElapsedEventArgs e)
+ {
+ // If we don't have an auth token, try to authorize
+ if (string.IsNullOrEmpty(this.authorizationToken))
+ {
+ if (!this.authorizationFailed)
+ {
+ // Only try once - the user needs to enable companion authorization in ytmdesktop
+ TextHandler.UpdateTextAndEmptyFilesMaybe(
+ string.Format(
+ CultureInfo.InvariantCulture,
+ LocalizedMessages.PlayerIsNotRunning,
+ LocalizedMessages.YouTubeMusic));
+
+ this.TryAuthorize();
+ }
+ return;
+ }
+
+ string downloadedJson = this.DownloadJson(this.companionServerAddress + "/api/v1/state");
+
+ if (!string.IsNullOrEmpty(downloadedJson))
+ {
+ try
+ {
+ dynamic jsonSummary = SimpleJson.DeserializeObject(downloadedJson);
+
+ if (jsonSummary == null)
+ {
+ this.ResetSnipSinceYouTubeMusicIsNotPlaying();
+ return;
+ }
+
+ dynamic playerObj = jsonSummary.player;
+ if (playerObj == null)
+ {
+ this.ResetSnipSinceYouTubeMusicIsNotPlaying();
+ return;
+ }
+
+ // trackState: -1=Unknown, 0=Paused, 1=Playing, 2=Buffering
+ object trackStateObj = playerObj.trackState;
+ if (trackStateObj == null)
+ {
+ this.ResetSnipSinceYouTubeMusicIsNotPlaying();
+ return;
+ }
+
+ long trackStateLong = Convert.ToInt64(trackStateObj, CultureInfo.InvariantCulture);
+
+ if (trackStateLong == 1) // Playing
+ {
+ dynamic videoObj = jsonSummary.video;
+ if (videoObj != null)
+ {
+ string videoId = string.Empty;
+ string title = string.Empty;
+ string artist = string.Empty;
+ string album = string.Empty;
+
+ if (videoObj.id != null)
+ {
+ videoId = videoObj.id.ToString();
+ }
+
+ if (videoObj.title != null)
+ {
+ title = videoObj.title.ToString();
+ }
+
+ if (videoObj.author != null)
+ {
+ artist = videoObj.author.ToString();
+ }
+
+ if (videoObj.album != null)
+ {
+ album = videoObj.album.ToString();
+ }
+
+ if (this.LastTitle != videoId)
+ {
+ // Track changed - update text
+ TextHandler.UpdateText(
+ title,
+ artist,
+ album,
+ videoId,
+ downloadedJson);
+
+ // Download album artwork if enabled
+ if (Globals.SaveAlbumArtwork)
+ {
+ this.DownloadAlbumArtwork(videoObj);
+ }
+
+ this.LastTitle = videoId;
+ }
+ }
+ else
+ {
+ this.ResetSnipSinceYouTubeMusicIsNotPlaying();
+ }
+ }
+ else
+ {
+ this.ResetSnipSinceYouTubeMusicIsNotPlaying();
+ }
+ }
+ catch
+ {
+ // Don't reset on parse errors — could be transient
+ }
+ }
+ else
+ {
+ // Download failed — don't reset. Keep showing last known track info.
+ // Only reset when we get a valid response indicating nothing is playing.
+ }
+ }
+
+ private void DownloadAlbumArtwork(dynamic videoInfo)
+ {
+ try
+ {
+ // The video info contains a thumbnails array with url, width, height
+ if (videoInfo.thumbnails != null)
+ {
+ // Find the best thumbnail based on the configured resolution
+ string imageUrl = SelectBestThumbnailUrl(videoInfo.thumbnails);
+
+ if (!string.IsNullOrEmpty(imageUrl))
+ {
+ using (WebClientWithShortTimeout webClient = new WebClientWithShortTimeout())
+ {
+ webClient.Headers.Add("User-Agent", "Snip/" + AssemblyInformation.AssemblyVersion);
+ webClient.DownloadFileAsync(new Uri(imageUrl), this.DefaultArtworkFilePath);
+ this.SavedBlankImage = false;
+ }
+ }
+ else
+ {
+ this.SaveBlankImage();
+ }
+ }
+ else
+ {
+ this.SaveBlankImage();
+ }
+ }
+ catch (WebException)
+ {
+ this.SaveBlankImage();
+ }
+ catch
+ {
+ this.SaveBlankImage();
+ }
+ }
+
+ private static string SelectBestThumbnailUrl(dynamic thumbnails)
+ {
+ // Thumbnails come as an array with url, width, height
+ // Select based on the configured artwork resolution
+ string bestUrl = string.Empty;
+ int targetSize = (int)Globals.ArtworkResolution;
+
+ if (targetSize == 0)
+ {
+ targetSize = 120; // Default to small
+ }
+
+ int bestDiff = int.MaxValue;
+
+ foreach (dynamic thumbnail in thumbnails)
+ {
+ int width = 0;
+ int height = 0;
+
+ if (thumbnail.width != null)
+ {
+ width = (int)(long)thumbnail.width;
+ }
+
+ if (thumbnail.height != null)
+ {
+ height = (int)(long)thumbnail.height;
+ }
+
+ int maxDimension = Math.Max(width, height);
+ int diff = Math.Abs(maxDimension - targetSize);
+
+ if (diff < bestDiff)
+ {
+ bestDiff = diff;
+ if (thumbnail.url != null)
+ {
+ bestUrl = thumbnail.url.ToString();
+ }
+ }
+ }
+
+ // If no good match found, just take the last one (usually largest)
+ if (string.IsNullOrEmpty(bestUrl))
+ {
+ foreach (dynamic thumbnail in thumbnails)
+ {
+ if (thumbnail.url != null)
+ {
+ bestUrl = thumbnail.url.ToString();
+ }
+ }
+ }
+
+ return bestUrl;
+ }
+
+ private string DownloadJson(string jsonAddress)
+ {
+ try
+ {
+ HttpWebRequest request = (HttpWebRequest)WebRequest.Create(jsonAddress);
+ request.Method = "GET";
+ request.Timeout = 10000;
+ request.UserAgent = "Snip/" + AssemblyInformation.AssemblyVersion;
+ request.Accept = "application/json";
+ request.KeepAlive = false;
+
+ if (!string.IsNullOrEmpty(this.authorizationToken))
+ {
+ request.Headers.Add("Authorization", this.authorizationToken);
+ }
+
+ using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
+ using (Stream responseStream = response.GetResponseStream())
+ using (StreamReader reader = new StreamReader(responseStream, Encoding.UTF8))
+ {
+ string downloadedJson = reader.ReadToEnd();
+
+ if (!string.IsNullOrEmpty(downloadedJson))
+ {
+ return downloadedJson;
+ }
+
+ return string.Empty;
+ }
+ }
+ catch (WebException webException)
+ {
+ HttpWebResponse response = webException.Response as HttpWebResponse;
+ if (response != null && response.StatusCode == HttpStatusCode.Forbidden)
+ {
+ // Token is invalid; clear it and try to re-authorize
+ this.authorizationToken = string.Empty;
+ this.authorizationFailed = false;
+ }
+
+ return string.Empty;
+ }
+ catch
+ {
+ return string.Empty;
+ }
+ }
+
+ private string PostJson(string address, string jsonBody, bool useAuth, int timeoutSeconds = 10)
+ {
+ using (WebClientWithConfigurableTimeout webClient = new WebClientWithConfigurableTimeout(timeoutSeconds))
+ {
+ try
+ {
+ webClient.Headers.Add("Content-Type", "application/json");
+ webClient.Headers.Add("User-Agent", "Snip/" + AssemblyInformation.AssemblyVersion);
+ webClient.Encoding = Encoding.UTF8;
+
+ if (useAuth && !string.IsNullOrEmpty(this.authorizationToken))
+ {
+ webClient.Headers.Add("Authorization", this.authorizationToken);
+ }
+
+ string result = webClient.UploadString(address, "POST", jsonBody);
+ return result;
+ }
+ catch (WebException)
+ {
+ return string.Empty;
+ }
+ }
+ }
+
+ private void SendCommand(string command)
+ {
+ try
+ {
+ string jsonBody = string.Format(
+ CultureInfo.InvariantCulture,
+ "{{\"command\":\"{0}\"}}",
+ command);
+
+ this.PostJson(this.companionServerAddress + "/api/v1/command", jsonBody, true);
+ }
+ catch
+ {
+ // Silently fail
+ }
+ }
+
+ private void ResetSnipSinceYouTubeMusicIsNotPlaying()
+ {
+ if (!this.SavedBlankImage)
+ {
+ if (Globals.SaveAlbumArtwork)
+ {
+ this.SaveBlankImage();
+ }
+ }
+
+ this.LastTitle = string.Empty;
+
+ TextHandler.UpdateTextAndEmptyFilesMaybe(LocalizedMessages.NoTrackPlaying);
+ }
+
+ #endregion
+
+ #region Player Control Methods
+
+ public override void ChangeToNextTrack()
+ {
+ this.SendCommand("next");
+ }
+
+ public override void ChangeToPreviousTrack()
+ {
+ this.SendCommand("previous");
+ }
+
+ public override void PlayOrPauseTrack()
+ {
+ this.SendCommand("playPause");
+ }
+
+ public override void PauseTrack()
+ {
+ this.SendCommand("pause");
+ }
+
+ public override void IncreasePlayerVolume()
+ {
+ this.SendCommand("volumeUp");
+ }
+
+ public override void DecreasePlayerVolume()
+ {
+ this.SendCommand("volumeDown");
+ }
+
+ public override void MutePlayerAudio()
+ {
+ this.SendCommand("mute");
+ }
+
+ #endregion
+
+ #region Classes
+
+ private class WebClientWithShortTimeout : WebClient
+ {
+ private const int WebClientTimeoutSeconds = 10;
+
+ protected override WebRequest GetWebRequest(Uri address)
+ {
+ WebRequest webRequest = base.GetWebRequest(address);
+ webRequest.Timeout = WebClientTimeoutSeconds * 1000;
+ return webRequest;
+ }
+ }
+
+ private class WebClientWithConfigurableTimeout : WebClient
+ {
+ private readonly int timeoutMs;
+
+ public WebClientWithConfigurableTimeout(int timeoutSeconds)
+ {
+ this.timeoutMs = timeoutSeconds * 1000;
+ }
+
+ protected override WebRequest GetWebRequest(Uri address)
+ {
+ WebRequest webRequest = base.GetWebRequest(address);
+ webRequest.Timeout = this.timeoutMs;
+ return webRequest;
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/Snip/Resources/Strings.txt b/Snip/Resources/Strings.txt
index df8699e..4526176 100644
--- a/Snip/Resources/Strings.txt
+++ b/Snip/Resources/Strings.txt
@@ -14,6 +14,7 @@ NewVersionAvailable=New Version Available!
NoPlayer=No Player Selected
Spotify=Spotify
Itunes=iTunes
+YouTubeMusic=YouTube Music
; This text is saved to the Snip.txt file when the user switches media players
; from the right-click context menu.
diff --git a/Snip/Snip.Designer.cs b/Snip/Snip.Designer.cs
index 6a9c0c8..f6be55c 100644
--- a/Snip/Snip.Designer.cs
+++ b/Snip/Snip.Designer.cs
@@ -9,6 +9,7 @@ public partial class Snip
private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemNoPlayer;
private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemSpotify;
private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemItunes;
+ private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemYouTubeMusic;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator1;
private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemSetFormat;
private System.Windows.Forms.ToolStripSeparator toolStripSeparator2;
@@ -67,6 +68,7 @@ private void InitializeComponent()
this.toolStripMenuItemNoPlayer = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripMenuItemSpotify = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripMenuItemItunes = new System.Windows.Forms.ToolStripMenuItem();
+ this.toolStripMenuItemYouTubeMusic = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator1 = new System.Windows.Forms.ToolStripSeparator();
this.toolStripMenuItemSetFormat = new System.Windows.Forms.ToolStripMenuItem();
this.toolStripSeparator2 = new System.Windows.Forms.ToolStripSeparator();
@@ -101,6 +103,7 @@ private void InitializeComponent()
this.toolStripMenuItemNoPlayer,
this.toolStripMenuItemSpotify,
this.toolStripMenuItemItunes,
+ this.toolStripMenuItemYouTubeMusic,
this.toolStripSeparator1,
this.toolStripMenuItemSetFormat,
this.toolStripSeparator2,
@@ -147,6 +150,13 @@ private void InitializeComponent()
this.toolStripMenuItemItunes.Text = LocalizedMessages.Itunes;
this.toolStripMenuItemItunes.Click += new System.EventHandler(this.PlayerSelectionCheck);
//
+ // toolStripMenuItemYouTubeMusic
+ //
+ this.toolStripMenuItemYouTubeMusic.Name = "toolStripMenuItemYouTubeMusic";
+ this.toolStripMenuItemYouTubeMusic.Size = new System.Drawing.Size(67, 22);
+ this.toolStripMenuItemYouTubeMusic.Text = LocalizedMessages.YouTubeMusic;
+ this.toolStripMenuItemYouTubeMusic.Click += new System.EventHandler(this.PlayerSelectionCheck);
+ //
// toolStripSeparator
//
this.toolStripSeparator.Name = "toolStripSeparator";
diff --git a/Snip/Snip.cs b/Snip/Snip.cs
index 4f7b5c2..fb12044 100644
--- a/Snip/Snip.cs
+++ b/Snip/Snip.cs
@@ -89,6 +89,7 @@ private static void SetLocalizedMessages()
LocalizedMessages.NoPlayer = Globals.ResourceManager.GetString("NoPlayer");
LocalizedMessages.Spotify = Globals.ResourceManager.GetString("Spotify");
LocalizedMessages.Itunes = Globals.ResourceManager.GetString("Itunes");
+ LocalizedMessages.YouTubeMusic = Globals.ResourceManager.GetString("YouTubeMusic") ?? "YouTube Music";
LocalizedMessages.SwitchedToPlayer = Globals.ResourceManager.GetString("SwitchedToPlayer");
LocalizedMessages.PlayerIsNotRunning = Globals.ResourceManager.GetString("PlayerIsNotRunning");
LocalizedMessages.NoTrackPlaying = Globals.ResourceManager.GetString("NoTrackPlaying");
@@ -233,13 +234,18 @@ private void PlayerSelectionCheck(object sender, EventArgs e)
{
this.TogglePlayer(Globals.MediaPlayerSelection.Itunes);
}
+ else if (sender == this.toolStripMenuItemYouTubeMusic)
+ {
+ this.TogglePlayer(Globals.MediaPlayerSelection.YouTubeMusic);
+ }
}
private void TogglePlayer(Globals.MediaPlayerSelection player)
{
- this.toolStripMenuItemNoPlayer.Checked = player == Globals.MediaPlayerSelection.NoPlayer;
- this.toolStripMenuItemSpotify.Checked = player == Globals.MediaPlayerSelection.Spotify;
- this.toolStripMenuItemItunes.Checked = player == Globals.MediaPlayerSelection.Itunes;
+ this.toolStripMenuItemNoPlayer.Checked = player == Globals.MediaPlayerSelection.NoPlayer;
+ this.toolStripMenuItemSpotify.Checked = player == Globals.MediaPlayerSelection.Spotify;
+ this.toolStripMenuItemItunes.Checked = player == Globals.MediaPlayerSelection.Itunes;
+ this.toolStripMenuItemYouTubeMusic.Checked = player == Globals.MediaPlayerSelection.YouTubeMusic;
Globals.CurrentPlayer.Unload();
string playerName = string.Empty;
@@ -258,6 +264,10 @@ private void TogglePlayer(Globals.MediaPlayerSelection player)
Globals.CurrentPlayer = new Itunes();
playerName = LocalizedMessages.Itunes;
break;
+ case Globals.MediaPlayerSelection.YouTubeMusic:
+ Globals.CurrentPlayer = new YouTubeMusic();
+ playerName = LocalizedMessages.YouTubeMusic;
+ break;
default:
break;
}
diff --git a/Snip/Snip.csproj b/Snip/Snip.csproj
index 1e1e2a2..82dcf1c 100644
--- a/Snip/Snip.csproj
+++ b/Snip/Snip.csproj
@@ -150,6 +150,7 @@
+
True
True