├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .gitmodules ├── App.config ├── Audio ├── AudioPlayer.cs ├── FFT.cs ├── Ogg.cs └── WebStreamPlayer.cs ├── Controls ├── AudioVisualiser.cs ├── BetterPictureBox.cs ├── BorderedPanel.cs ├── CenterPanel.cs ├── GhostTextbox.cs ├── MarqueeLabel.cs └── Meiryo.cs ├── FormSettings.Designer.cs ├── FormSettings.cs ├── FormSettings.resx ├── Globals.cs ├── ILMerge.props ├── ILMergeOrder.txt ├── LICENSE ├── ListenMoe.exe.config ├── ListenMoeClient.csproj ├── ListenMoeClient.csproj.user ├── ListenMoeClient.sln ├── MainForm.Designer.cs ├── MainForm.cs ├── MainForm.resx ├── Program.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs └── Resources.resx ├── README.md ├── RawInput.cs ├── Resources ├── DiscordRPC.dll ├── Meiryo.ttf ├── close_inverted.png ├── cog_inverted.png ├── fav_sprite.png ├── gripper-inverted.png ├── gripper.png ├── heart_sprite.png ├── icon.ico ├── pause.png ├── pause_ico.ico ├── pause_inverted.png ├── play.png ├── play_ico.ico └── play_inverted.png ├── Settings.cs ├── SpriteLoader.cs ├── Web ├── ReadFullyStream.cs ├── SongInfo.cs ├── Updater.cs ├── User.cs └── WebHelper.cs └── packages.config /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | **Please describe the problem you are having in as much detail as possible:** 8 | 9 | **Further details:** 10 | 11 | - Operating system (considering this is Windows only, just supply a number): 12 | - Priority this should have – please be realistic and elaborate if possible: 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | obj/ 2 | bin/ 3 | icon.png 4 | icon.psd 5 | .vs/ 6 | CrappyListenMoe.csproj.user 7 | packages/ 8 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "CsGrid"] 2 | path = CsGrid 3 | url = https://github.com/anonymousthing/CsGrid.git 4 | -------------------------------------------------------------------------------- /App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Audio/AudioPlayer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using NAudio.Wave; 5 | using NAudio.Wave.SampleProviders; 6 | 7 | namespace ListenMoeClient 8 | { 9 | /// 10 | /// Audio Output Device Object 11 | /// 12 | public class AudioDevice 13 | { 14 | public DirectSoundDeviceInfo DeviceInfo; 15 | public string Name; 16 | 17 | public AudioDevice(DirectSoundDeviceInfo deviceInfo, string name) 18 | { 19 | DeviceInfo = deviceInfo; 20 | Name = name; 21 | } 22 | 23 | public override string ToString() => Name; 24 | } 25 | 26 | public class AudioPlayer : IDisposable 27 | { 28 | BufferedWaveProvider provider; 29 | DirectSoundOut directOut; 30 | SampleChannel volumeChannel; 31 | public Guid CurrentDeviceGuid { get; private set; } = Guid.NewGuid(); 32 | 33 | readonly Queue samplesToPlay = new Queue(); 34 | 35 | public AudioPlayer() 36 | { 37 | WaveFormat format = new WaveFormat(Globals.SAMPLE_RATE, 2); 38 | provider = new BufferedWaveProvider(format) 39 | { 40 | BufferDuration = TimeSpan.FromSeconds(10) 41 | }; 42 | 43 | volumeChannel = new SampleChannel(provider) 44 | { 45 | Volume = Settings.Get(Setting.Volume) 46 | }; 47 | 48 | bool success = Guid.TryParse(Settings.Get(Setting.OutputDeviceGuid), out Guid deviceGuid); 49 | 50 | SetAudioOutputDevice(success ? deviceGuid : DirectSoundOut.DSDEVID_DefaultPlayback); 51 | } 52 | 53 | /// 54 | /// Intialize the WaveOut Device and set Volume 55 | /// 56 | public void Initialize(Guid deviceGuid) 57 | { 58 | directOut = new DirectSoundOut(deviceGuid); 59 | this.CurrentDeviceGuid = deviceGuid; 60 | directOut.Init(volumeChannel); 61 | 62 | Settings.Set(Setting.OutputDeviceGuid, deviceGuid.ToString()); 63 | Settings.WriteSettings(); 64 | } 65 | 66 | public void Play() 67 | { 68 | provider.ClearBuffer(); 69 | directOut.Play(); 70 | } 71 | 72 | public void Stop() 73 | { 74 | directOut.Stop(); 75 | provider.ClearBuffer(); 76 | } 77 | 78 | public void Dispose() 79 | { 80 | if (directOut != null) 81 | { 82 | directOut.Stop(); 83 | directOut.Dispose(); 84 | } 85 | 86 | if (provider != null) 87 | provider.ClearBuffer(); 88 | } 89 | 90 | public void QueueBuffer(short[] samples) 91 | { 92 | byte[] bytes = new byte[samples.Length * 2]; 93 | Buffer.BlockCopy(samples, 0, bytes, 0, bytes.Length); 94 | 95 | try 96 | { 97 | provider.AddSamples(bytes, 0, bytes.Length); 98 | } 99 | catch (Exception e) 100 | { 101 | provider.ClearBuffer(); 102 | provider.AddSamples(bytes, 0, bytes.Length); 103 | } 104 | } 105 | 106 | private float BoundVolume(float vol) 107 | { 108 | //Cap between 0 and 1 109 | vol = Math.Max(0, vol); 110 | vol = Math.Min(1, vol); 111 | return vol; 112 | } 113 | 114 | public float AddVolume(float vol) 115 | { 116 | SetVolume(volumeChannel.Volume + vol); 117 | return volumeChannel.Volume; 118 | } 119 | 120 | public void SetVolume(float vol) => volumeChannel.Volume = BoundVolume(vol); 121 | 122 | /// 123 | /// Get an array of the available audio output devices. 124 | /// Because of a limitation of WaveOut, device's names will be cut if they are too long. 125 | /// 126 | public AudioDevice[] GetAudioOutputDevices() => DirectSoundOut.Devices.Select(d => new AudioDevice(d, d.Description)).ToArray(); 127 | 128 | /// 129 | /// Set the audio output device (if available); Returns current audio device (desired if valid). 130 | /// 131 | /// Device ID 132 | /// 133 | public void SetAudioOutputDevice(Guid deviceGuid) 134 | { 135 | if (deviceGuid == this.CurrentDeviceGuid) 136 | return; 137 | 138 | PlaybackState prevState = directOut?.PlaybackState ?? PlaybackState.Playing; 139 | 140 | if (directOut != null) 141 | { 142 | directOut.Stop(); 143 | directOut.Dispose(); 144 | } 145 | 146 | Initialize(deviceGuid); 147 | 148 | if (prevState == PlaybackState.Playing) 149 | directOut.Play(); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Audio/FFT.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ListenMoeClient 4 | { 5 | public struct Complex 6 | { 7 | public float Real, Imaginary; 8 | 9 | public Complex(float r, float i) 10 | { 11 | Real = r; 12 | Imaginary = i; 13 | } 14 | 15 | public double Magnitude => Math.Sqrt(Real * Real + Imaginary * Imaginary); 16 | 17 | public static Complex operator *(Complex a, Complex b) => new Complex(a.Real * b.Real - a.Imaginary * b.Imaginary, 18 | a.Real * b.Imaginary + a.Imaginary * b.Real); 19 | 20 | public static Complex operator +(Complex a, Complex b) => new Complex(a.Real + b.Real, a.Imaginary + b.Imaginary); 21 | 22 | public static Complex operator -(Complex a, Complex b) => new Complex(a.Real - b.Real, a.Imaginary - b.Imaginary); 23 | } 24 | 25 | class FFT 26 | { 27 | private static Complex[] ConvertToComplex(short[] buffer) 28 | { 29 | Complex[] complex = new Complex[buffer.Length]; 30 | for (int i = 0; i < buffer.Length; i++) 31 | { 32 | complex[i] = new Complex(buffer[i], 0); 33 | } 34 | return complex; 35 | } 36 | 37 | public static float[] Fft(short[] buffer, int exponent) 38 | { 39 | Complex[] data = ConvertToComplex(buffer); 40 | Fft(data, buffer.Length, exponent); 41 | 42 | float[] resultBuffer = new float[data.Length / 2]; 43 | for (int i = 1; i < resultBuffer.Length; i++) 44 | { 45 | resultBuffer[i] = (float)data[i].Magnitude / 64; 46 | } 47 | return resultBuffer; 48 | } 49 | 50 | public static void Fft(Complex[] data, int fftSize, int exponent) 51 | { 52 | int c = fftSize; 53 | 54 | //binary inversion 55 | Inverse(data, c); 56 | 57 | int j0, j1, j2 = 1; 58 | float n0, n1, tr, ti, m; 59 | float v0 = -1, v1 = 0; 60 | 61 | //move to outer scope to optimize performance 62 | int j, i; 63 | 64 | for (int l = 0; l < exponent; l++) 65 | { 66 | n0 = 1; 67 | n1 = 0; 68 | j1 = j2; 69 | j2 <<= 1; //j2 * 2 70 | 71 | for (j = 0; j < j1; j++) 72 | { 73 | for (i = j; i < c; i += j2) 74 | { 75 | j0 = i + j1; 76 | //-- 77 | tr = n0 * data[j0].Real - n1 * data[j0].Imaginary; 78 | ti = n0 * data[j0].Imaginary + n1 * data[j0].Real; 79 | //-- 80 | data[j0].Real = data[i].Real - tr; 81 | data[j0].Imaginary = data[i].Imaginary - ti; 82 | //add 83 | data[i].Real += tr; 84 | data[i].Imaginary += ti; 85 | } 86 | 87 | //calc coeff 88 | m = v0 * n0 - v1 * n1; 89 | n1 = v1 * n0 + v0 * n1; 90 | n0 = m; 91 | } 92 | 93 | v1 = (float)Math.Sqrt((1f - v0) / 2f); 94 | v0 = (float)Math.Sqrt((1f + v0) / 2f); 95 | } 96 | 97 | for (int k = 0; k < c; k++) 98 | { 99 | data[k].Real /= c; 100 | data[k].Imaginary /= c; 101 | } 102 | } 103 | 104 | private static void Inverse(Complex[] data, int c) 105 | { 106 | int z = 0; 107 | int n1 = c >> 1; //c / 2 108 | 109 | for (int n0 = 0; n0 < c - 1; n0++) 110 | { 111 | if (n0 < z) 112 | { 113 | Swap(data, n0, z); 114 | } 115 | int l = n1; 116 | 117 | while (l <= z) 118 | { 119 | z = z - l; 120 | l >>= 1; 121 | } 122 | z += l; 123 | } 124 | } 125 | 126 | private static void Swap(Complex[] data, int index, int index2) 127 | { 128 | Complex tmp = data[index]; 129 | data[index] = data[index2]; 130 | data[index2] = tmp; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /Audio/Ogg.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace ListenMoeClient 6 | { 7 | class Ogg 8 | { 9 | readonly byte[] magic = { 0x4F, 0x67, 0x67, 0x53 }; //OggS 10 | 11 | //Pre gen'd CRC lookup table cause why not: http://barrgroup.com/Embedded-Systems/How-To/CRC-Calculation-C-Code 12 | readonly uint[] crcLookup = { 13 | 0x00000000, 0x04C11DB7, 0x09823B6E, 0x0D4326D9, 14 | 0x130476DC, 0x17C56B6B, 0x1A864DB2, 0x1E475005, 15 | 0x2608EDB8, 0x22C9F00F, 0x2F8AD6D6, 0x2B4BCB61, 16 | 0x350C9B64, 0x31CD86D3, 0x3C8EA00A, 0x384FBDBD, 17 | 0x4C11DB70, 0x48D0C6C7, 0x4593E01E, 0x4152FDA9, 18 | 0x5F15ADAC, 0x5BD4B01B, 0x569796C2, 0x52568B75, 19 | 0x6A1936C8, 0x6ED82B7F, 0x639B0DA6, 0x675A1011, 20 | 0x791D4014, 0x7DDC5DA3, 0x709F7B7A, 0x745E66CD, 21 | 0x9823B6E0, 0x9CE2AB57, 0x91A18D8E, 0x95609039, 22 | 0x8B27C03C, 0x8FE6DD8B, 0x82A5FB52, 0x8664E6E5, 23 | 0xBE2B5B58, 0xBAEA46EF, 0xB7A96036, 0xB3687D81, 24 | 0xAD2F2D84, 0xA9EE3033, 0xA4AD16EA, 0xA06C0B5D, 25 | 0xD4326D90, 0xD0F37027, 0xDDB056FE, 0xD9714B49, 26 | 0xC7361B4C, 0xC3F706FB, 0xCEB42022, 0xCA753D95, 27 | 0xF23A8028, 0xF6FB9D9F, 0xFBB8BB46, 0xFF79A6F1, 28 | 0xE13EF6F4, 0xE5FFEB43, 0xE8BCCD9A, 0xEC7DD02D, 29 | 0x34867077, 0x30476DC0, 0x3D044B19, 0x39C556AE, 30 | 0x278206AB, 0x23431B1C, 0x2E003DC5, 0x2AC12072, 31 | 0x128E9DCF, 0x164F8078, 0x1B0CA6A1, 0x1FCDBB16, 32 | 0x018AEB13, 0x054BF6A4, 0x0808D07D, 0x0CC9CDCA, 33 | 0x7897AB07, 0x7C56B6B0, 0x71159069, 0x75D48DDE, 34 | 0x6B93DDDB, 0x6F52C06C, 0x6211E6B5, 0x66D0FB02, 35 | 0x5E9F46BF, 0x5A5E5B08, 0x571D7DD1, 0x53DC6066, 36 | 0x4D9B3063, 0x495A2DD4, 0x44190B0D, 0x40D816BA, 37 | 0xACA5C697, 0xA864DB20, 0xA527FDF9, 0xA1E6E04E, 38 | 0xBFA1B04B, 0xBB60ADFC, 0xB6238B25, 0xB2E29692, 39 | 0x8AAD2B2F, 0x8E6C3698, 0x832F1041, 0x87EE0DF6, 40 | 0x99A95DF3, 0x9D684044, 0x902B669D, 0x94EA7B2A, 41 | 0xE0B41DE7, 0xE4750050, 0xE9362689, 0xEDF73B3E, 42 | 0xF3B06B3B, 0xF771768C, 0xFA325055, 0xFEF34DE2, 43 | 0xC6BCF05F, 0xC27DEDE8, 0xCF3ECB31, 0xCBFFD686, 44 | 0xD5B88683, 0xD1799B34, 0xDC3ABDED, 0xD8FBA05A, 45 | 0x690CE0EE, 0x6DCDFD59, 0x608EDB80, 0x644FC637, 46 | 0x7A089632, 0x7EC98B85, 0x738AAD5C, 0x774BB0EB, 47 | 0x4F040D56, 0x4BC510E1, 0x46863638, 0x42472B8F, 48 | 0x5C007B8A, 0x58C1663D, 0x558240E4, 0x51435D53, 49 | 0x251D3B9E, 0x21DC2629, 0x2C9F00F0, 0x285E1D47, 50 | 0x36194D42, 0x32D850F5, 0x3F9B762C, 0x3B5A6B9B, 51 | 0x0315D626, 0x07D4CB91, 0x0A97ED48, 0x0E56F0FF, 52 | 0x1011A0FA, 0x14D0BD4D, 0x19939B94, 0x1D528623, 53 | 0xF12F560E, 0xF5EE4BB9, 0xF8AD6D60, 0xFC6C70D7, 54 | 0xE22B20D2, 0xE6EA3D65, 0xEBA91BBC, 0xEF68060B, 55 | 0xD727BBB6, 0xD3E6A601, 0xDEA580D8, 0xDA649D6F, 56 | 0xC423CD6A, 0xC0E2D0DD, 0xCDA1F604, 0xC960EBB3, 57 | 0xBD3E8D7E, 0xB9FF90C9, 0xB4BCB610, 0xB07DABA7, 58 | 0xAE3AFBA2, 0xAAFBE615, 0xA7B8C0CC, 0xA379DD7B, 59 | 0x9B3660C6, 0x9FF77D71, 0x92B45BA8, 0x9675461F, 60 | 0x8832161A, 0x8CF30BAD, 0x81B02D74, 0x857130C3, 61 | 0x5D8A9099, 0x594B8D2E, 0x5408ABF7, 0x50C9B640, 62 | 0x4E8EE645, 0x4A4FFBF2, 0x470CDD2B, 0x43CDC09C, 63 | 0x7B827D21, 0x7F436096, 0x7200464F, 0x76C15BF8, 64 | 0x68860BFD, 0x6C47164A, 0x61043093, 0x65C52D24, 65 | 0x119B4BE9, 0x155A565E, 0x18197087, 0x1CD86D30, 66 | 0x029F3D35, 0x065E2082, 0x0B1D065B, 0x0FDC1BEC, 67 | 0x3793A651, 0x3352BBE6, 0x3E119D3F, 0x3AD08088, 68 | 0x2497D08D, 0x2056CD3A, 0x2D15EBE3, 0x29D4F654, 69 | 0xC5A92679, 0xC1683BCE, 0xCC2B1D17, 0xC8EA00A0, 70 | 0xD6AD50A5, 0xD26C4D12, 0xDF2F6BCB, 0xDBEE767C, 71 | 0xE3A1CBC1, 0xE760D676, 0xEA23F0AF, 0xEEE2ED18, 72 | 0xF0A5BD1D, 0xF464A0AA, 0xF9278673, 0xFDE69BC4, 73 | 0x89B8FD09, 0x8D79E0BE, 0x803AC667, 0x84FBDBD0, 74 | 0x9ABC8BD5, 0x9E7D9662, 0x933EB0BB, 0x97FFAD0C, 75 | 0xAFB010B1, 0xAB710D06, 0xA6322BDF, 0xA2F33668, 76 | 0xBCB4666D, 0xB8757BDA, 0xB5365D03, 0xB1F740B4 77 | }; 78 | 79 | private List buffer = new List(); 80 | 81 | private byte[] ReadBytes(Stream stream, int length) 82 | { 83 | byte[] bytes = new byte[length]; 84 | stream.Read(bytes, 0, length); 85 | return bytes; 86 | } 87 | 88 | private uint CalculateChecksum(List pageBytes) 89 | { 90 | uint crc = 0; 91 | 92 | for (int i = 0; i < pageBytes.Count; i++) 93 | crc = (crc << 8) ^ crcLookup[((crc >> 24) & 0xFF) ^ pageBytes[i]]; 94 | 95 | return crc; 96 | } 97 | 98 | //Reads until the "OggS" magic string. Resyncs stream if we miss bytes. 99 | private void ReadUntilMagic(Stream stream) 100 | { 101 | int currentIndex = 0; 102 | while (true) 103 | { 104 | if (currentIndex == 4) 105 | break; 106 | 107 | if (stream.ReadByte() == magic[currentIndex]) 108 | currentIndex++; 109 | else 110 | currentIndex = 0; 111 | } 112 | } 113 | 114 | List pageBytes = new List(); //For checksum check 115 | private List ReadPage(Stream stream) 116 | { 117 | pageBytes.Clear(); 118 | 119 | ReadUntilMagic(stream); //Magic word 120 | pageBytes.AddRange(magic); 121 | int vn = stream.ReadByte(); //Ogg version number (0) 122 | pageBytes.Add((byte)vn); 123 | 124 | int packetFlag = stream.ReadByte(); //Packet flag, we ignore ¯\_(ツ)_/¯ 125 | pageBytes.Add((byte)packetFlag); 126 | 127 | byte[] granulePosition = ReadBytes(stream, 8); //Granule position 128 | pageBytes.AddRange(granulePosition); 129 | 130 | byte[] streamSN = ReadBytes(stream, 4); 131 | pageBytes.AddRange(streamSN); 132 | 133 | byte[] pageSN = ReadBytes(stream, 4); 134 | pageBytes.AddRange(pageSN); 135 | 136 | byte[] crc = ReadBytes(stream, 4); 137 | pageBytes.AddRange(new byte[] { 0, 0, 0, 0}); 138 | 139 | int segmentCount = stream.ReadByte(); //Number of segments in this page, 0-255 140 | pageBytes.Add((byte)segmentCount); 141 | 142 | int[] segmentLengths = new int[segmentCount]; 143 | //Read segment table 144 | for (int i = 0; i < segmentCount; i++) 145 | { 146 | segmentLengths[i] = stream.ReadByte(); 147 | pageBytes.Add((byte)segmentLengths[i]); 148 | } 149 | 150 | List data = new List(); 151 | //Read segments 152 | for (int i = 0; i < segmentCount; i++) 153 | { 154 | byte[] segmentBytes = ReadBytes(stream, segmentLengths[i]); 155 | pageBytes.AddRange(segmentBytes); 156 | buffer.AddRange(segmentBytes); 157 | if (segmentLengths[i] < 255) 158 | { 159 | data.Add(buffer.ToArray()); 160 | buffer.Clear(); 161 | } 162 | } 163 | 164 | //Skip page if CRC does not match 165 | uint calculatedCrc = CalculateChecksum(pageBytes); 166 | if (calculatedCrc != BitConverter.ToUInt32(crc, 0)) 167 | return new List(); 168 | 169 | return data; 170 | } 171 | 172 | public byte[][] GetAudioPackets(Stream stream) 173 | { 174 | List packets = new List(); 175 | while (true) 176 | { 177 | packets.AddRange(ReadPage(stream)); 178 | if (packets.Count > 0) 179 | break; 180 | } 181 | 182 | return packets.ToArray(); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Audio/WebStreamPlayer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Concentus.Structs; 6 | 7 | namespace ListenMoeClient 8 | { 9 | class WebStreamPlayer 10 | { 11 | public AudioPlayer BasePlayer { get; set; } = new AudioPlayer(); 12 | 13 | Thread provideThread; 14 | 15 | Ogg ogg = new Ogg(); 16 | OpusDecoder decoder = OpusDecoder.Create(Globals.SAMPLE_RATE, 2); 17 | 18 | bool playing = false; 19 | string url; 20 | 21 | AudioVisualiser visualiser; 22 | 23 | public WebStreamPlayer(string url) => this.url = url; 24 | 25 | public async Task Dispose() => await Stop(); 26 | 27 | public void SetVisualiser(AudioVisualiser visualiser) => this.visualiser = visualiser; 28 | 29 | public void SetStreamUrl(string url) => this.url = url; 30 | 31 | public void Play() 32 | { 33 | this.BasePlayer.Play(); 34 | playing = true; 35 | 36 | provideThread = new Thread(() => 37 | { 38 | try 39 | { 40 | WebClient wc = new WebClient(); 41 | wc.Headers[HttpRequestHeader.UserAgent] = Globals.USER_AGENT; 42 | 43 | using (System.IO.Stream stream = wc.OpenRead(url)) 44 | { 45 | ReadFullyStream readFullyStream = new ReadFullyStream(stream); 46 | 47 | int packetCounter = 0; 48 | while (playing) 49 | { 50 | byte[][] packets = ogg.GetAudioPackets(readFullyStream); 51 | 52 | packetCounter++; 53 | //Skip first 5 pages (control frames, etc) 54 | if (packetCounter <= 5) 55 | continue; 56 | 57 | for (int i = 0; i < packets.Length; i++) 58 | { 59 | byte[] streamBytes = packets[i]; 60 | try 61 | { 62 | int frameSize = OpusPacketInfo.GetNumSamplesPerFrame(streamBytes, 0, Globals.SAMPLE_RATE); //Get frame size from opus packet 63 | short[] rawBuffer = new short[frameSize * 2]; //2 channels 64 | int buffer = decoder.Decode(streamBytes, 0, streamBytes.Length, rawBuffer, 0, frameSize, false); 65 | this.BasePlayer.QueueBuffer(rawBuffer); 66 | 67 | if (visualiser != null) 68 | visualiser.AddSamples(rawBuffer); 69 | } 70 | catch (Concentus.OpusException) 71 | { 72 | //Skip this frame 73 | } 74 | } 75 | } 76 | } 77 | } 78 | catch (Exception) 79 | { 80 | 81 | } 82 | }); 83 | provideThread.Start(); 84 | } 85 | 86 | public float AddVolume(float vol) => this.BasePlayer.AddVolume(vol); 87 | 88 | public async Task Stop() 89 | { 90 | if (playing) 91 | { 92 | playing = false; 93 | 94 | this.BasePlayer.Stop(); 95 | 96 | if (provideThread != null) 97 | { 98 | provideThread.Abort(); 99 | await Task.Run(() => provideThread.Join()); 100 | provideThread = null; 101 | } 102 | 103 | decoder.ResetState(); 104 | } 105 | } 106 | 107 | public bool IsPlaying() => playing; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Controls/AudioVisualiser.cs: -------------------------------------------------------------------------------- 1 | using DequeNet; 2 | using System; 3 | using System.Drawing; 4 | using System.Drawing.Drawing2D; 5 | using System.Linq; 6 | 7 | namespace ListenMoeClient 8 | { 9 | public class AudioVisualiser 10 | { 11 | DateTime anchor; 12 | Deque sampleBuffer = new Deque(); 13 | readonly int fftSize = Settings.Get(Setting.FftSize); 14 | const int exponent = 11; 15 | 16 | float[] lastFftPoints; 17 | readonly bool logarithmic = false; 18 | readonly float bias = 0.3f; 19 | readonly float ScaleFactor = 1f; 20 | readonly float normalisationFactor = 0.9f; 21 | 22 | int resolutionFactor = Settings.Get(Setting.VisualiserResolutionFactor); //higher = lower resolution, number is the number of samples to skip 23 | float barWidth = Settings.Get(Setting.VisualiserBarWidth); 24 | 25 | bool bars = true; 26 | bool stopped = true; 27 | 28 | Rectangle Bounds = Rectangle.Empty; 29 | Color visualiserColor; 30 | Brush barBrush; 31 | Pen linePen; 32 | 33 | public AudioVisualiser() => lastFftPoints = new float[fftSize]; 34 | 35 | public void SetBounds(Rectangle bounds) => Bounds = bounds; 36 | 37 | public void ReloadSettings() 38 | { 39 | bars = Settings.Get(Setting.VisualiserBars); 40 | int opacity = (int)Math.Min(Math.Max(Settings.Get(Setting.VisualiserOpacity) * 255, 0), 255); 41 | Color baseVisualiserColor = Settings.Get(Setting.StreamType) == StreamType.Jpop ? Settings.Get(Setting.JPOPVisualiserColor) : Settings.Get(Setting.KPOPVisualiserColor); 42 | Color visualiserCol = Settings.Get(Setting.CustomColors) ? Settings.Get(Setting.CustomVisualiserColor) : baseVisualiserColor; 43 | visualiserColor = Color.FromArgb(opacity, visualiserCol); 44 | 45 | if (Bounds.Width == 0 || Bounds.Height == 0) 46 | return; 47 | 48 | if (Settings.Get(Setting.VisualiserFadeEdges)) 49 | { 50 | Color baseColor = Settings.Get(Setting.StreamType) == StreamType.Jpop ? Settings.Get(Setting.JPOPBaseColor) : Settings.Get(Setting.KPOPBaseColor); 51 | Color color = Settings.Get(Setting.CustomColors) ? Settings.Get(Setting.CustomBaseColor) : baseColor; 52 | barBrush = new LinearGradientBrush(new Rectangle(Point.Empty, Bounds.Size), color, visualiserColor, LinearGradientMode.Horizontal); 53 | ColorBlend blend = new ColorBlend 54 | { 55 | Colors = new Color[] { color, color, visualiserColor, visualiserColor, visualiserColor, color, color }, 56 | Positions = new float[] { 0.0f, 0.05f, 0.2f, 0.5f, 0.8f, 0.95f, 1.0f } 57 | }; 58 | ((LinearGradientBrush)barBrush).InterpolationColors = blend; 59 | linePen = new Pen(barBrush, 1); 60 | } 61 | else 62 | { 63 | barBrush = new SolidBrush(visualiserColor); 64 | linePen = new Pen(visualiserColor, 1); 65 | } 66 | } 67 | 68 | public void AddSamples(short[] samples) 69 | { 70 | if (stopped) 71 | return; 72 | 73 | if (anchor == DateTime.MinValue) 74 | anchor = DateTime.Now; 75 | 76 | foreach (short s in samples) 77 | sampleBuffer.PushRight(s); 78 | 79 | if (sampleBuffer.Count > fftSize * 4) 80 | { 81 | DateTime now = DateTime.Now; 82 | int currentPos = (int)(Globals.SAMPLE_RATE * ((now - anchor).TotalMilliseconds / 1000)) * 2; 83 | anchor = now; 84 | for (int i = 0; i < currentPos && sampleBuffer.Count > 0; i++) 85 | sampleBuffer.PopLeft(); 86 | } 87 | } 88 | 89 | public void ClearBuffers() 90 | { 91 | anchor = DateTime.MinValue; 92 | sampleBuffer.Clear(); 93 | } 94 | 95 | public void Stop() 96 | { 97 | stopped = true; 98 | sampleBuffer.Clear(); 99 | anchor = DateTime.MinValue; 100 | } 101 | 102 | public void Start() => stopped = false; 103 | 104 | public void IncreaseBarWidth(float amount) 105 | { 106 | barWidth += amount; 107 | Settings.Set(Setting.VisualiserBarWidth, barWidth); 108 | Settings.WriteSettings(); 109 | } 110 | 111 | public void IncreaseResolution(int amount) 112 | { 113 | if (resolutionFactor - amount > 0) 114 | { 115 | resolutionFactor -= amount; 116 | Settings.Set(Setting.VisualiserResolutionFactor, resolutionFactor); 117 | Settings.WriteSettings(); 118 | } 119 | } 120 | 121 | private float[] CalculateNextFftFrame() 122 | { 123 | if (sampleBuffer == null) 124 | return null; 125 | 126 | int currentPos = (int)(Globals.SAMPLE_RATE * ((DateTime.Now - anchor).TotalMilliseconds / 1000)) * 2; 127 | if (sampleBuffer.Count - currentPos < fftSize || currentPos < 0) 128 | return null; 129 | 130 | short[] window = new short[fftSize]; 131 | for (int i = 0; i < fftSize; i++) 132 | window[i] = sampleBuffer[currentPos + i]; 133 | 134 | ApplyWindowFunction(window); 135 | float[] bins = FFT.Fft(window, exponent); 136 | bins = bins.Take(bins.Length / 4).ToArray(); 137 | bins = bins.Select(f => (float)Math.Log10(f * 10) * 2 + 1).Select(f => ((f - 0.3f) * 1.5f) + 1.5f).ToArray(); 138 | return bins; 139 | } 140 | 141 | private void ApplyWindowFunction(short[] data) 142 | { 143 | for (int i = 0; i < data.Length; i++) 144 | { 145 | data[i] = (short)(data[i] * 0.5 * (1 - Math.Cos(2 * Math.PI * i / data.Length))); 146 | } 147 | } 148 | 149 | public void Render(Graphics g) 150 | { 151 | float[] fftPoints; 152 | if (stopped) 153 | { 154 | fftPoints = new float[lastFftPoints.Length]; 155 | } 156 | else 157 | { 158 | fftPoints = CalculateNextFftFrame(); 159 | if (fftPoints == null) 160 | { 161 | fftPoints = lastFftPoints; 162 | } 163 | } 164 | 165 | //Process points 166 | int noPoints = fftPoints.Length / resolutionFactor; 167 | PointF[] points = new PointF[noPoints]; 168 | 169 | for (int i = 1; i < fftPoints.Length; i++) 170 | fftPoints[i] = fftPoints[i] * bias + lastFftPoints[i] * (1 - bias); 171 | 172 | int j = 0; 173 | if (logarithmic) 174 | { 175 | float binWidth = (Globals.SAMPLE_RATE / 2) / fftPoints.Length; 176 | float maxNote = (float)(12 * Math.Log(((fftPoints.Length - 1) * binWidth) / 16.35, 10)); 177 | for (int i = 1; j < noPoints && i < fftPoints.Length; i += resolutionFactor) 178 | { 179 | float yVal = fftPoints[i]; 180 | yVal *= Bounds.Height * ScaleFactor * 0.1f; 181 | yVal = yVal + ((yVal * normalisationFactor * j / noPoints) - (yVal * normalisationFactor / 2)); 182 | if (float.IsInfinity(yVal) || float.IsNaN(yVal)) 183 | yVal = 0; 184 | 185 | //TODO: cache this 186 | float frequency = i * binWidth; 187 | float notePos = (float)(12 * Math.Log(frequency / 16.35f, 10)); 188 | float xVal = Bounds.Width * (notePos / maxNote); 189 | 190 | points[j] = new PointF(xVal, yVal); 191 | j++; 192 | } 193 | points[0] = new PointF(0, points[1].Y); 194 | } 195 | else 196 | { 197 | float spacing = Bounds.Width / ((float)noPoints - 1); 198 | for (int i = 0; j < noPoints && i < fftPoints.Length; i += resolutionFactor) 199 | { 200 | float yVal = fftPoints[i]; 201 | yVal *= Bounds.Height * ScaleFactor * 0.1f; 202 | yVal = yVal + ((yVal * normalisationFactor * j / noPoints) - (yVal * normalisationFactor / 2)); 203 | if (float.IsInfinity(yVal) || float.IsNaN(yVal)) 204 | yVal = 0; 205 | points[j] = new PointF(spacing * j, yVal); 206 | j++; 207 | } 208 | points[0] = new PointF(0, points[1].Y); 209 | } 210 | 211 | g.SmoothingMode = SmoothingMode.HighQuality; 212 | 213 | float scale = Settings.Get(Setting.Scale); 214 | 215 | //Bins go from bottom to top 216 | g.TranslateTransform(0, Bounds.Height); 217 | g.ScaleTransform(1, -1); 218 | 219 | g.TranslateTransform(Bounds.X, 0); 220 | if (bars) 221 | { 222 | RectangleF[] rectangles = new RectangleF[points.Length - 1]; 223 | for (int i = 0; i < points.Length - 1; i++) 224 | { 225 | PointF next = points[i + 1]; 226 | PointF current = points[i]; 227 | 228 | float pos = Math.Max(current.X, current.X + (next.X - current.X - barWidth * scale) / 2); 229 | float width = Math.Min(barWidth * scale, next.X - current.X); 230 | rectangles[i] = new RectangleF(pos, 0, width, current.Y); 231 | } 232 | g.FillRectangles(barBrush, rectangles); 233 | } 234 | else 235 | { 236 | g.DrawCurve(linePen, points); 237 | } 238 | g.TranslateTransform(-Bounds.X, 0); 239 | 240 | g.ScaleTransform(1, -1); 241 | g.TranslateTransform(0, -(Bounds.Height)); 242 | 243 | lastFftPoints = fftPoints.Select(x => (float.IsInfinity(x) || float.IsNaN(x)) ? 0 : x).ToArray(); 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /Controls/BetterPictureBox.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Forms; 2 | 3 | namespace ListenMoeClient 4 | { 5 | class BetterPictureBox : PictureBox 6 | { 7 | protected override void OnPaint(PaintEventArgs pe) 8 | { 9 | pe.Graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; 10 | base.OnPaint(pe); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Controls/BorderedPanel.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | using System.Windows.Forms; 3 | 4 | namespace ListenMoeClient.Controls 5 | { 6 | class BorderedPanel : Panel 7 | { 8 | public Color BorderColor { get; set; } = Color.Black; 9 | 10 | private Pen borderPen = new Pen(Color.Black, 1); 11 | private int borderWidth = 1; 12 | public int BorderWidth 13 | { 14 | get 15 | { 16 | return borderWidth; 17 | } 18 | set 19 | { 20 | borderWidth = value; 21 | borderPen = new Pen(BorderColor, borderWidth); 22 | } 23 | } 24 | 25 | protected override void OnPaint(PaintEventArgs e) 26 | { 27 | base.OnPaint(e); 28 | e.Graphics.DrawRectangle(borderPen, borderWidth / 2, borderWidth / 2, ClientRectangle.Width - borderWidth, ClientRectangle.Height - borderWidth); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Controls/CenterPanel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows.Forms; 8 | 9 | namespace ListenMoeClient 10 | { 11 | enum UpdateState 12 | { 13 | None, 14 | InProgress, 15 | Complete 16 | } 17 | 18 | class CenterPanel : Panel 19 | { 20 | public AudioVisualiser Visualiser = new AudioVisualiser(); 21 | 22 | MarqueeLabel lblArtist = new MarqueeLabel(); 23 | MarqueeLabel lblTitle = new MarqueeLabel(); 24 | MarqueeLabel lblEvent = new MarqueeLabel(); 25 | int eventBarHeight = 16; 26 | bool isEventOrRequest = false; 27 | 28 | float updatePercent = 0; 29 | UpdateState updateState; 30 | 31 | public CenterPanel() 32 | { 33 | SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); 34 | 35 | lblArtist.Text = "Connecting..."; 36 | lblEvent.Centered = true; 37 | RecalculateMarqueeBounds(); 38 | 39 | Visualiser.SetBounds(new Rectangle(0, 0, this.Width, this.Height)); 40 | Visualiser.ReloadSettings(); 41 | } 42 | 43 | private void RecalculateMarqueeBounds() 44 | { 45 | float scale = Settings.Get(Setting.Scale); 46 | 47 | lblEvent.Bounds = new Rectangle(0, (int)(scale + this.Height - eventBarHeight * scale), this.Width, (int)(eventBarHeight * scale)); 48 | if (isEventOrRequest) 49 | { 50 | lblArtist.Bounds = new Rectangle((int)(8 * scale), (int)(4 * scale), this.Width, this.Height); 51 | lblTitle.Bounds = new Rectangle((int)(6 * scale), (int)(18 * scale), this.Width, this.Height); 52 | } 53 | else 54 | { 55 | lblArtist.Bounds = new Rectangle((int)(8 * scale), (int)(13 * scale), this.Width, this.Height); 56 | lblTitle.Bounds = new Rectangle((int)(6 * scale), (int)(28 * scale), this.Width, this.Height); 57 | } 58 | 59 | lblEvent.RecalculateBounds(); 60 | lblArtist.RecalculateBounds(); 61 | lblTitle.RecalculateBounds(); 62 | } 63 | 64 | protected override void OnResize(EventArgs eventargs) 65 | { 66 | base.OnResize(eventargs); 67 | 68 | RecalculateMarqueeBounds(); 69 | Visualiser.SetBounds(new Rectangle(0, 0, this.Width, this.Height)); 70 | } 71 | 72 | protected override void OnPaint(PaintEventArgs e) 73 | { 74 | base.OnPaint(e); 75 | 76 | this.SuspendLayout(); 77 | 78 | Visualiser.Render(e.Graphics); 79 | lblTitle.Render(e.Graphics); 80 | lblArtist.Render(e.Graphics); 81 | 82 | if (isEventOrRequest) 83 | { 84 | float scale = Settings.Get(Setting.Scale); 85 | 86 | Brush brush = new SolidBrush(Color.FromArgb(64, 0, 0, 0)); 87 | e.Graphics.FillRectangle(brush, 0, this.Height - eventBarHeight * scale, this.Width, eventBarHeight * scale); 88 | lblEvent.Render(e.Graphics); 89 | } 90 | 91 | if (updateState != 0) 92 | { 93 | Brush brush = new SolidBrush(updateState == UpdateState.InProgress ? Color.Yellow : Color.LimeGreen); 94 | //Height for pause/play button 95 | e.Graphics.FillRectangle(brush, 0, this.Height - 3, this.Width * updatePercent, 3); 96 | } 97 | 98 | this.ResumeLayout(); 99 | } 100 | 101 | public void SetUpdateState(UpdateState state) 102 | { 103 | updateState = state; 104 | this.Invalidate(); 105 | } 106 | 107 | public void SetUpdatePercent(float updatePercent) 108 | { 109 | this.updatePercent = updatePercent; 110 | this.Invalidate(); 111 | } 112 | 113 | public void SetLabelText(string titleText, string artistText, string albumText, string eventText, bool isEventOrRequest) 114 | { 115 | lblTitle.Text = titleText; 116 | lblArtist.Text = artistText; 117 | if (!string.IsNullOrWhiteSpace(albumText)) 118 | lblArtist.Text += " - " + albumText; 119 | lblEvent.Text = eventText; 120 | 121 | this.isEventOrRequest = isEventOrRequest; 122 | RecalculateMarqueeBounds(); 123 | } 124 | 125 | public void SetLabelBrush(Brush brush) 126 | { 127 | lblTitle.FontBrush = brush; 128 | lblArtist.FontBrush = brush; 129 | lblEvent.FontBrush = brush; 130 | } 131 | 132 | public void SetFonts(Font titleFont, Font albumFont) 133 | { 134 | lblTitle.Font = titleFont; 135 | lblTitle.Subfont = albumFont; 136 | lblArtist.Font = albumFont; 137 | lblEvent.Font = albumFont; 138 | 139 | lblTitle.RecalculateBounds(); 140 | lblArtist.RecalculateBounds(); 141 | lblEvent.RecalculateBounds(); 142 | } 143 | 144 | public void StartVisualiser(WebStreamPlayer player) 145 | { 146 | Visualiser.Start(); 147 | player.SetVisualiser(Visualiser); 148 | } 149 | 150 | public void StopVisualiser(WebStreamPlayer player) 151 | { 152 | player.SetVisualiser(null); 153 | Visualiser.Stop(); 154 | } 155 | 156 | public void ReloadVisualiser() 157 | { 158 | Visualiser.ReloadSettings(); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /Controls/GhostTextbox.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Windows.Forms; 4 | 5 | namespace ListenMoeClient 6 | { 7 | public class GhostTextbox : TextBox 8 | { 9 | [DllImport("user32.dll", EntryPoint = "SendMessageW")] 10 | public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wp, IntPtr lp); 11 | 12 | private string ghostText; 13 | /// 14 | /// The placeholder text to display in the textbox. 15 | /// 16 | public string GhostText 17 | { 18 | get { return ghostText; } 19 | set { ghostText = value; updateGhostText(); } 20 | } 21 | 22 | public GhostTextbox() { } 23 | 24 | public GhostTextbox(string ghostText) 25 | { 26 | GhostText = ghostText; 27 | } 28 | 29 | protected override void OnHandleCreated(EventArgs e) 30 | { 31 | base.OnHandleCreated(e); 32 | updateGhostText(); 33 | } 34 | 35 | private void updateGhostText() 36 | { 37 | if (!this.IsHandleCreated || string.IsNullOrWhiteSpace(ghostText)) 38 | return; 39 | 40 | IntPtr mem = Marshal.StringToHGlobalUni(ghostText); 41 | SendMessage(this.Handle, 0x1501, (IntPtr)1, mem); 42 | Marshal.FreeHGlobal(mem); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Controls/MarqueeLabel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Drawing.Text; 4 | 5 | namespace ListenMoeClient 6 | { 7 | class MarqueeLabel 8 | { 9 | public float ScrollSpeed { get; set; } = 50; //In pixels per second 10 | public bool Centered { get; set; } = false; 11 | private readonly bool renderBounds = false; //For debugging 12 | readonly Pen boundsPen = new Pen(new SolidBrush(Globals.RandomColor())); 13 | 14 | private string text = ""; 15 | public string Text 16 | { 17 | get => text; 18 | set 19 | { 20 | text = value; 21 | RecalculateBounds(); 22 | } 23 | } 24 | 25 | private string subtext = ""; 26 | public string Subtext 27 | { 28 | get => subtext; 29 | set 30 | { 31 | subtext = value; 32 | RecalculateBounds(); 33 | } 34 | } 35 | 36 | private float currentPosition = 0; 37 | private readonly float spacing = 0.7f; //As a multiple of the width of the label 38 | private readonly float subtextDistance = 3; //In pixels 39 | 40 | private DateTime last; 41 | 42 | private SizeF mainTextSize; 43 | private SizeF subTextSize; 44 | private float totalStringWidth; 45 | 46 | bool scrolling = false; 47 | 48 | public Rectangle Bounds = Rectangle.Empty; 49 | public Font Font = new Font("Segoe UI", 9); 50 | public Font Subfont = new Font("Segoe UI", 8); 51 | public Brush FontBrush = Brushes.White; 52 | 53 | private bool textChanged = true; 54 | 55 | private void UpdateTextPosition() 56 | { 57 | DateTime current = DateTime.Now; 58 | double ms = (current - last).TotalMilliseconds; 59 | 60 | if (scrolling) 61 | { 62 | float distance = (float)(this.ScrollSpeed * (ms / 1000)); 63 | currentPosition -= distance; 64 | if (currentPosition < -totalStringWidth) 65 | currentPosition = Bounds.Width * spacing; 66 | } 67 | 68 | last = current; 69 | } 70 | 71 | public void RecalculateBounds() => textChanged = true; 72 | 73 | public void Render(Graphics g) 74 | { 75 | float scale = Settings.Get(Setting.Scale); 76 | if (textChanged) 77 | { 78 | mainTextSize = g.MeasureString(text, Font); 79 | subTextSize = g.MeasureString(subtext, Subfont); 80 | 81 | totalStringWidth = mainTextSize.Width; 82 | if (subtext.Trim() != "") 83 | { 84 | totalStringWidth += subtextDistance; //Spacing between main and subtext 85 | totalStringWidth += subTextSize.Width; 86 | } 87 | totalStringWidth += 2; //Padding 88 | 89 | if (totalStringWidth > Bounds.Width) 90 | scrolling = true; 91 | else 92 | { 93 | scrolling = false; 94 | currentPosition = 0; 95 | } 96 | textChanged = false; 97 | } 98 | 99 | if (scale < 2) 100 | g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; 101 | else 102 | g.TextRenderingHint = TextRenderingHint.AntiAlias; 103 | 104 | UpdateTextPosition(); 105 | 106 | g.TranslateTransform((int)currentPosition, 0); 107 | float x; 108 | if (this.Centered) 109 | x = (Bounds.Width / 2 - totalStringWidth / 2); 110 | else 111 | x = Bounds.Location.X; 112 | RectangleF rect = new RectangleF(new PointF(x, Bounds.Location.Y), new SizeF(totalStringWidth, Bounds.Height)); 113 | 114 | void DrawText() 115 | { 116 | g.DrawString(text, Font, FontBrush, rect.Location); 117 | if (subtext.Trim() != "") 118 | { 119 | g.DrawString(subtext, Subfont, FontBrush, new PointF(x + mainTextSize.Width + subtextDistance * scale, rect.Location.Y + ((mainTextSize.Height - subTextSize.Height) / 2))); 120 | } 121 | } 122 | 123 | DrawText(); 124 | 125 | if (scrolling) 126 | { 127 | //Draw it on the other side for seamless looping 128 | float secondPosition = x + totalStringWidth + Bounds.Width * spacing; 129 | g.TranslateTransform(secondPosition, 0); 130 | 131 | DrawText(); 132 | 133 | g.TranslateTransform(-secondPosition, 0); 134 | } 135 | 136 | g.TranslateTransform(-(int)currentPosition, 0); 137 | 138 | if (renderBounds) 139 | { 140 | g.DrawRectangle(boundsPen, Bounds); 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Controls/Meiryo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Drawing.Text; 5 | using System.Runtime.InteropServices; 6 | 7 | namespace ListenMoeClient 8 | { 9 | class Meiryo 10 | { 11 | private static PrivateFontCollection fonts = new PrivateFontCollection(); 12 | 13 | private static Dictionary fontCache = new Dictionary(); 14 | 15 | static Meiryo() 16 | { 17 | byte[] fontData = Properties.Resources.Meiryo; 18 | GCHandle handle = GCHandle.Alloc(fontData, GCHandleType.Pinned); 19 | IntPtr pointer = handle.AddrOfPinnedObject(); 20 | try 21 | { 22 | fonts.AddMemoryFont(pointer, fontData.Length); 23 | } 24 | finally 25 | { 26 | handle.Free(); 27 | } 28 | } 29 | 30 | public static Font GetFont(float size) 31 | { 32 | if (fontCache.ContainsKey(size)) 33 | return fontCache[size]; 34 | 35 | Font font = new Font(fonts.Families[0], size, GraphicsUnit.Point); 36 | fontCache.Add(size, font); 37 | return font; 38 | } 39 | 40 | public static FontFamily GetFontFamily() => fonts.Families[0]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /FormSettings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Drawing; 4 | using System.Windows.Forms; 5 | using System.Threading.Tasks; 6 | 7 | namespace ListenMoeClient 8 | { 9 | 10 | 11 | public partial class FormSettings : Form 12 | { 13 | MainForm mainForm; 14 | AudioPlayer audioPlayer; 15 | 16 | public FormSettings(MainForm mainForm, AudioPlayer audioPlayer) 17 | { 18 | InitializeComponent(); 19 | this.Icon = Properties.Resources.icon; 20 | this.mainForm = mainForm; 21 | this.audioPlayer = audioPlayer; 22 | 23 | LoadAndBindCheckboxSetting(cbCloseToTray, "CloseToTray"); 24 | LoadAndBindCheckboxSetting(cbEnableVisualiser, "EnableVisualiser"); 25 | LoadAndBindCheckboxSetting(cbHideFromAltTab, "HideFromAltTab"); 26 | LoadAndBindCheckboxSetting(cbUpdateAutocheck, "UpdateAutocheck"); 27 | LoadAndBindCheckboxSetting(cbThumbnailButton, "ThumbnailButton"); 28 | LoadAndBindCheckboxSetting(cbTopmost, "TopMost"); 29 | LoadAndBindCheckboxSetting(cbVisualiserBars, "VisualiserBars"); 30 | LoadAndBindCheckboxSetting(cbVisualiserFadeEdges, "VisualiserFadeEdges"); 31 | 32 | LoadAndBindColorSetting(panelVisualiserColor, "CustomVisualiserColor"); 33 | LoadAndBindColorSetting(panelBaseColor, "CustomBaseColor"); 34 | LoadAndBindColorSetting(panelAccentColor, "CustomAccentColor"); 35 | 36 | numericUpdateInterval.Value = Settings.Get(Setting.UpdateInterval) / 60; 37 | numericUpdateInterval.ValueChanged += NumericUpdateInterval_ValueChanged; 38 | 39 | float scale = Settings.Get(Setting.Scale); 40 | tbResolutionScale.Value = (int)(scale * 10); 41 | lblResolutionScale.Text = scale.ToString("N1"); 42 | 43 | float visualiserOpacity = Settings.Get(Setting.VisualiserOpacity); 44 | tbVisualiserOpacity.Value = (int)(visualiserOpacity * 255); 45 | lblVisualiserOpacity.Text = visualiserOpacity.ToString("N1"); 46 | 47 | float opacity = Settings.Get(Setting.FormOpacity); 48 | tbOpacity.Value = (int)(opacity * 255); 49 | lblOpacity.Text = opacity.ToString("N1"); 50 | 51 | panelNotLoggedIn.Visible = !User.LoggedIn; 52 | panelLoggedIn.Visible = User.LoggedIn; 53 | lblLoginStatus.Text = "Logged in as " + Settings.Get(Setting.Username); 54 | lblLoginStatus.Location = new Point((this.Width / 2) - (lblLoginStatus.Width / 2), lblLoginStatus.Location.Y); 55 | 56 | StreamType st = Settings.Get(Setting.StreamType); 57 | rbKpop.Checked = st == StreamType.Kpop; 58 | 59 | User.OnLoginComplete += () => 60 | { 61 | lblLoginStatus.Text = "Logged in as " + Settings.Get(Setting.Username); 62 | lblLoginStatus.Location = new Point((this.Width / 2) - (lblLoginStatus.Width / 2), lblLoginStatus.Location.Y); 63 | txtUsername.Clear(); 64 | txtPassword.Clear(); 65 | panelNotLoggedIn.Visible = false; 66 | panelTwoFactorAuth.Visible = false; 67 | panelLoggedIn.Visible = true; 68 | panelLoggedIn.BringToFront(); 69 | }; 70 | User.OnLogout += () => 71 | { 72 | panelLoggedIn.Visible = false; 73 | panelNotLoggedIn.Visible = true; 74 | panelNotLoggedIn.BringToFront(); 75 | }; 76 | 77 | reloadAudioDevices(); 78 | } 79 | 80 | private void NumericUpdateInterval_ValueChanged(object sender, EventArgs e) 81 | { 82 | Settings.Set(Setting.UpdateInterval, (int)numericUpdateInterval.Value * 60); 83 | Settings.WriteSettings(); 84 | } 85 | 86 | private void LoadAndBindCheckboxSetting(CheckBox checkbox, string settingsKey) 87 | { 88 | Setting key = (Setting)Enum.Parse(typeof(Setting), settingsKey); 89 | checkbox.Checked = Settings.Get(key); 90 | checkbox.CheckStateChanged += (sender, e) => 91 | { 92 | Settings.Set(key, checkbox.Checked); 93 | Settings.WriteSettings(); 94 | mainForm.ReloadSettings(); 95 | }; 96 | } 97 | 98 | private void LoadAndBindColorSetting(Panel panel, string settingsKey) 99 | { 100 | Setting key = (Setting)Enum.Parse(typeof(Setting), settingsKey); 101 | panel.BackColor = Settings.Get(key); 102 | panel.MouseClick += (sender, e) => 103 | { 104 | ColorDialog dialog = new ColorDialog(); 105 | if (dialog.ShowDialog() == DialogResult.OK) 106 | { 107 | Color c = dialog.Color; 108 | panel.BackColor = c; 109 | 110 | Settings.Set(key, c); 111 | Settings.Set(Setting.CustomColors, true); 112 | Settings.WriteSettings(); 113 | 114 | mainForm.ReloadSettings(); 115 | } 116 | }; 117 | } 118 | 119 | private void tbResolutionScale_Scroll(object sender, EventArgs e) 120 | { 121 | //Set new scale 122 | float newScale = tbResolutionScale.Value / 10f; 123 | lblResolutionScale.Text = newScale.ToString("N1"); 124 | Settings.Set(Setting.Scale, newScale); 125 | Settings.WriteSettings(); 126 | 127 | //Reload form scaling 128 | mainForm.ReloadScale(); 129 | } 130 | 131 | private void FormSettings_FormClosing(object sender, FormClosingEventArgs e) => 132 | //Should probably use a mutex for this, but oh well 133 | mainForm.SettingsForm = null; 134 | 135 | private async void btnLogin_Click(object sender, EventArgs e) 136 | { 137 | btnLogin.Enabled = false; 138 | AuthenticateResponse response = await User.Login(txtUsername.Text, txtPassword.Text); 139 | btnLogin.Enabled = true; 140 | if (response.mfa) 141 | { 142 | panelNotLoggedIn.Visible = false; 143 | panelLoggedIn.Visible = false; 144 | panelTwoFactorAuth.Visible = true; 145 | panelTwoFactorAuth.BringToFront(); 146 | txtTwoFactorAuthCode.Focus(); 147 | } 148 | } 149 | 150 | private void btnLogout_Click(object sender, EventArgs e) => User.Logout(); 151 | 152 | private void tbVisualiserOpacity_Scroll(object sender, EventArgs e) 153 | { 154 | float newVal = tbVisualiserOpacity.Value / 255f; 155 | lblVisualiserOpacity.Text = newVal.ToString("N1"); 156 | Settings.Set(Setting.VisualiserOpacity, newVal); 157 | Settings.WriteSettings(); 158 | 159 | mainForm.ReloadSettings(); 160 | } 161 | 162 | private void tbOpacity_Scroll(object sender, EventArgs e) 163 | { 164 | float newVal = tbOpacity.Value / 255f; 165 | lblOpacity.Text = newVal.ToString("N1"); 166 | Settings.Set(Setting.FormOpacity, newVal); 167 | Settings.WriteSettings(); 168 | 169 | mainForm.ReloadSettings(); 170 | } 171 | 172 | private void reloadAudioDevices() 173 | { 174 | dropdownAudioDevices.DataSource = audioPlayer.GetAudioOutputDevices(); 175 | dropdownAudioDevices.SelectedIndex = Math.Max(0, Array.IndexOf(audioPlayer.GetAudioOutputDevices().Select(a => a.DeviceInfo.Guid).ToArray(), audioPlayer.CurrentDeviceGuid)); 176 | } 177 | 178 | private void cbAudioDevices_SelectionChangeCommitted(object sender, EventArgs e) 179 | { 180 | AudioDevice selected = (AudioDevice)dropdownAudioDevices.SelectedItem; 181 | audioPlayer.SetAudioOutputDevice(selected.DeviceInfo.Guid); 182 | } 183 | 184 | private void btnRefreshAudioDevices_Click(object sender, EventArgs e) => reloadAudioDevices(); 185 | 186 | private void txtPassword_KeyPress(object sender, KeyPressEventArgs e) 187 | { 188 | if (e.KeyChar == (char)Keys.Enter) 189 | { 190 | e.Handled = true; 191 | 192 | btnLogin.PerformClick(); 193 | } 194 | } 195 | 196 | private async void btnTwoFactorAuthSubmit_Click(object sender, EventArgs e) 197 | { 198 | btnTwoFactorAuthSubmit.Enabled = false; 199 | bool success = await User.LoginMfa(txtTwoFactorAuthCode.Text); 200 | btnTwoFactorAuthSubmit.Enabled = true; 201 | if (!success) 202 | { 203 | lblIncorrectTwoFactorAuth.Visible = true; 204 | await Task.Delay(2000); 205 | lblIncorrectTwoFactorAuth.Visible = false; 206 | } 207 | } 208 | 209 | private void txtTwoFactorAuthCode_KeyPress(object sender, KeyPressEventArgs e) 210 | { 211 | if (e.KeyChar == (char)Keys.Enter) 212 | { 213 | e.Handled = true; 214 | btnTwoFactorAuthSubmit.PerformClick(); 215 | } 216 | } 217 | 218 | private async Task ReloadStreamType() 219 | { 220 | StreamType current = Settings.Get(Setting.StreamType); 221 | StreamType next = rbJpop.Checked ? StreamType.Jpop : StreamType.Kpop; 222 | 223 | if (current != next) 224 | { 225 | Settings.Set(Setting.StreamType, next); 226 | Settings.WriteSettings(); 227 | 228 | mainForm.ReloadSettings(); 229 | await mainForm.ReloadStream(); 230 | } 231 | } 232 | 233 | private async void rbKpop_CheckedChanged(object sender, EventArgs e) => await ReloadStreamType(); 234 | 235 | private async void rbJpop_CheckedChanged(object sender, EventArgs e) => await ReloadStreamType(); 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /FormSettings.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 | -------------------------------------------------------------------------------- /Globals.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Windows.Forms; 5 | 6 | namespace ListenMoeClient 7 | { 8 | static class Globals 9 | { 10 | public static string VERSION = Application.ProductVersion.Substring(0, Application.ProductVersion.LastIndexOf('.')); //Strip build number 11 | public static string USER_AGENT = "LISTEN.moe Desktop Client v" + VERSION + " (https://github.com/anonymousthing/ListenMoeClient)"; 12 | public static int SAMPLE_RATE = 48000; 13 | 14 | static Random r = new Random(); 15 | 16 | public static Point Subtract(this Point a, Point b) => new Point(a.X - b.X, a.Y - b.Y); 17 | 18 | public static float Bound(this float f, float min, float max) => Math.Max(Math.Min(f, max), min); 19 | 20 | public static byte BoundToByte(this float f) => (byte)(Math.Min(Math.Max(0, f), 255)); 21 | 22 | public static Color Scale(this Color color, float multiplier) => Color.FromArgb( 23 | (color.R * multiplier).BoundToByte(), 24 | (color.G * multiplier).BoundToByte(), 25 | (color.B * multiplier).BoundToByte() 26 | ); 27 | 28 | public static Rectangle Scale(this Rectangle r, float f) => new Rectangle((int)(r.X * f), (int)(r.Y * f), (int)(r.Width * f), (int)(r.Height * f)); 29 | 30 | public static Color RandomColor() => Color.FromArgb(r.Next(255), r.Next(255), r.Next(255)); 31 | 32 | public static Rectangle ToRectangle(this RectangleF r) => new Rectangle((int)r.X, (int)r.Y, (int)r.Width, (int)r.Height); 33 | 34 | public static Point ToPoint(this PointF p) => new Point((int)p.X, (int)p.Y); 35 | 36 | static Dictionary originalRect = new Dictionary(); 37 | static Dictionary originalMinSize = new Dictionary(); 38 | public static void BetterScale(this Control c, float f) 39 | { 40 | c.SuspendLayout(); 41 | 42 | if (!originalRect.ContainsKey(c)) 43 | { 44 | originalRect[c] = new Rectangle(c.Location.X, c.Location.Y, c.Width, c.Height); 45 | originalMinSize[c] = c.MinimumSize; 46 | } 47 | 48 | Size origMinSize = originalMinSize[c]; 49 | c.MinimumSize = new SizeF(origMinSize.Width * f, origMinSize.Height * f).ToSize(); 50 | 51 | Rectangle origRect = originalRect[c]; 52 | c.Width = (int)(origRect.Width * f); 53 | c.Height = (int)(origRect.Height * f); 54 | if (!(c is Form)) 55 | c.Location = new PointF(origRect.X * f, origRect.Y * f).ToPoint(); 56 | 57 | //Scale children 58 | foreach (Control c2 in c.Controls) 59 | BetterScale(c2, f); 60 | 61 | c.ResumeLayout(); 62 | } 63 | 64 | public static void ResetScale(this Control c) 65 | { 66 | if (originalRect.ContainsKey(c)) 67 | { 68 | c.MinimumSize = originalMinSize[c]; 69 | 70 | Rectangle origRect = originalRect[c]; 71 | c.Width = origRect.Width; 72 | c.Height = origRect.Height; 73 | c.Location = origRect.Location; 74 | 75 | //Reset children 76 | foreach (Control c2 in c.Controls) 77 | ResetScale(c2); 78 | 79 | originalRect.Remove(c); 80 | originalMinSize.Remove(c); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ILMerge.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | true 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 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /ILMergeOrder.txt: -------------------------------------------------------------------------------- 1 | # this file contains the partial list of the merged assemblies in the merge order 2 | # you can fill it from the obj\CONFIG\PROJECT.ilmerge generated on every build 3 | # and finetune merge order to your satisfaction 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sammy Sammon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ListenMoe.exe.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /ListenMoeClient.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Debug 7 | AnyCPU 8 | {88B02799-425E-4622-A849-202ADB19601B} 9 | WinExe 10 | Properties 11 | ListenMoeClient 12 | ListenMoeClient 13 | v4.5.2 14 | 512 15 | true 16 | 17 | 18 | false 19 | publish\ 20 | true 21 | Disk 22 | false 23 | Foreground 24 | 7 25 | Days 26 | false 27 | false 28 | true 29 | 0 30 | 1.0.0.0 31 | false 32 | true 33 | true 34 | 35 | 36 | AnyCPU 37 | true 38 | full 39 | false 40 | bin\Debug\ 41 | DEBUG;TRACE 42 | prompt 43 | 4 44 | false 45 | latest 46 | 47 | 48 | AnyCPU 49 | pdbonly 50 | true 51 | bin\Release\ 52 | TRACE 53 | prompt 54 | 4 55 | false 56 | latest 57 | 58 | 59 | Resources\icon.ico 60 | 61 | 62 | 63 | 64 | 65 | F8DAFDD0A24E871FDC496F12140F14DC080D408D 66 | 67 | 68 | ListenMoeClient_TemporaryKey.pfx 69 | 70 | 71 | false 72 | 73 | 74 | LocalIntranet 75 | 76 | 77 | 78 | false 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | packages\Concentus.1.1.7\lib\portable-net45+win+wpa81+wp80\Concentus.dll 88 | 89 | 90 | packages\DequeNet.1.0.2\lib\netstandard1.1\DequeNet.dll 91 | 92 | 93 | False 94 | Resources\DiscordRPC.dll 95 | 96 | 97 | packages\Microsoft.WindowsAPICodePack-Core.1.1.0.2\lib\Microsoft.WindowsAPICodePack.dll 98 | 99 | 100 | packages\Microsoft.WindowsAPICodePack-Shell.1.1.0.0\lib\Microsoft.WindowsAPICodePack.Shell.dll 101 | 102 | 103 | packages\Microsoft.WindowsAPICodePack-Shell.1.1.0.0\lib\Microsoft.WindowsAPICodePack.ShellExtensions.dll 104 | 105 | 106 | packages\NAudio.1.9.0-preview1\lib\net35\NAudio.dll 107 | 108 | 109 | packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll 119 | 120 | 121 | packages\System.ValueTuple.4.4.0\lib\netstandard1.0\System.ValueTuple.dll 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | packages\WebSocketSharp.1.0.3-rc11\lib\websocket-sharp.dll 135 | True 136 | 137 | 138 | 139 | 140 | 141 | 142 | Component 143 | 144 | 145 | Component 146 | 147 | 148 | Component 149 | 150 | 151 | Form 152 | 153 | 154 | MainForm.cs 155 | 156 | 157 | Component 158 | 159 | 160 | Form 161 | 162 | 163 | FormSettings.cs 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | True 172 | True 173 | Resources.resx 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | FormSettings.cs 188 | 189 | 190 | MainForm.cs 191 | 192 | 193 | ResXFileCodeGenerator 194 | Designer 195 | Resources.Designer.cs 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | False 232 | Microsoft .NET Framework 4.5.2 %28x86 and x64%29 233 | true 234 | 235 | 236 | False 237 | .NET Framework 3.5 SP1 238 | false 239 | 240 | 241 | 242 | 243 | {B2BADD28-F832-4B85-AFEE-08AE5B3B784C} 244 | CsGrid 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 255 | 256 | 257 | 258 | 259 | 260 | 267 | -------------------------------------------------------------------------------- /ListenMoeClient.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ProjectFiles 5 | publish\ 6 | 7 | 8 | 9 | 10 | 11 | en-US 12 | false 13 | 14 | 15 | false 16 | 17 | -------------------------------------------------------------------------------- /ListenMoeClient.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2010 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ListenMoeClient", "ListenMoeClient.csproj", "{88B02799-425E-4622-A849-202ADB19601B}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CsGrid", "CsGrid\CsGrid\CsGrid.csproj", "{B2BADD28-F832-4B85-AFEE-08AE5B3B784C}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {88B02799-425E-4622-A849-202ADB19601B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {88B02799-425E-4622-A849-202ADB19601B}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {88B02799-425E-4622-A849-202ADB19601B}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {88B02799-425E-4622-A849-202ADB19601B}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {B2BADD28-F832-4B85-AFEE-08AE5B3B784C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {B2BADD28-F832-4B85-AFEE-08AE5B3B784C}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {B2BADD28-F832-4B85-AFEE-08AE5B3B784C}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {B2BADD28-F832-4B85-AFEE-08AE5B3B784C}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {0BDAB897-D09D-4B51-B8BA-9E14ACB06B60} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /MainForm.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace ListenMoeClient 2 | { 3 | partial class MainForm 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.components = new System.ComponentModel.Container(); 32 | this.contextMenu1 = new System.Windows.Forms.ContextMenu(); 33 | this.menuItemCopySongInfo = new System.Windows.Forms.MenuItem(); 34 | this.menuItemOptions = new System.Windows.Forms.MenuItem(); 35 | this.menuItem_Close = new System.Windows.Forms.MenuItem(); 36 | this.notifyIcon1 = new System.Windows.Forms.NotifyIcon(this.components); 37 | this.contextMenu2 = new System.Windows.Forms.ContextMenu(); 38 | this.menuItemPlayPause = new System.Windows.Forms.MenuItem(); 39 | this.menuItemShow = new System.Windows.Forms.MenuItem(); 40 | this.menuItemResetLocation = new System.Windows.Forms.MenuItem(); 41 | this.menuItemClose2 = new System.Windows.Forms.MenuItem(); 42 | this.gridPanel = new CsGrid.GridPanel(); 43 | this.panelPlayBtn = new System.Windows.Forms.Panel(); 44 | this.panelRight = new System.Windows.Forms.Panel(); 45 | this.lblVol = new System.Windows.Forms.Label(); 46 | this.picPlayPause = new ListenMoeClient.BetterPictureBox(); 47 | this.centerPanel = new ListenMoeClient.CenterPanel(); 48 | this.coverImage = new ListenMoeClient.BetterPictureBox(); 49 | this.picFavourite = new ListenMoeClient.BetterPictureBox(); 50 | this.gridPanel.SuspendLayout(); 51 | this.panelPlayBtn.SuspendLayout(); 52 | this.panelRight.SuspendLayout(); 53 | ((System.ComponentModel.ISupportInitialize)(this.picPlayPause)).BeginInit(); 54 | this.centerPanel.SuspendLayout(); 55 | ((System.ComponentModel.ISupportInitialize)(this.coverImage)).BeginInit(); 56 | ((System.ComponentModel.ISupportInitialize)(this.picFavourite)).BeginInit(); 57 | this.SuspendLayout(); 58 | // 59 | // contextMenu1 60 | // 61 | this.contextMenu1.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { 62 | this.menuItemCopySongInfo, 63 | this.menuItemOptions, 64 | this.menuItem_Close}); 65 | // 66 | // menuItemCopySongInfo 67 | // 68 | this.menuItemCopySongInfo.Index = 0; 69 | this.menuItemCopySongInfo.Text = "Copy song info"; 70 | this.menuItemCopySongInfo.Click += new System.EventHandler(this.menuItemCopySongInfo_Click); 71 | // 72 | // menuItemOptions 73 | // 74 | this.menuItemOptions.Index = 1; 75 | this.menuItemOptions.Text = "Options"; 76 | this.menuItemOptions.Click += new System.EventHandler(this.menuItemOptions_Click); 77 | // 78 | // menuItem_Close 79 | // 80 | this.menuItem_Close.Index = 2; 81 | this.menuItem_Close.Text = "Close"; 82 | this.menuItem_Close.Click += new System.EventHandler(this.menuItemClose_Click); 83 | // 84 | // notifyIcon1 85 | // 86 | this.notifyIcon1.Text = "Listen.moe"; 87 | this.notifyIcon1.Visible = true; 88 | this.notifyIcon1.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.notifyIcon1_MouseDoubleClick); 89 | // 90 | // contextMenu2 91 | // 92 | this.contextMenu2.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { 93 | this.menuItemPlayPause, 94 | this.menuItemShow, 95 | this.menuItemResetLocation, 96 | this.menuItemClose2}); 97 | // 98 | // menuItemPlayPause 99 | // 100 | this.menuItemPlayPause.Index = 0; 101 | this.menuItemPlayPause.Text = "Pause"; 102 | this.menuItemPlayPause.Click += new System.EventHandler(this.playPause_Click); 103 | // 104 | // menuItemShow 105 | // 106 | this.menuItemShow.Index = 1; 107 | this.menuItemShow.Text = "Show"; 108 | this.menuItemShow.Click += new System.EventHandler(this.menuItemShow_Click); 109 | // 110 | // menuItemResetLocation 111 | // 112 | this.menuItemResetLocation.Index = 2; 113 | this.menuItemResetLocation.Text = "Reset location"; 114 | this.menuItemResetLocation.Click += new System.EventHandler(this.menuItemResetLocation_Click); 115 | // 116 | // menuItemClose2 117 | // 118 | this.menuItemClose2.Index = 3; 119 | this.menuItemClose2.Text = "Close"; 120 | this.menuItemClose2.Click += new System.EventHandler(this.menuItemClose2_Click); 121 | // 122 | // gridPanel 123 | // 124 | this.gridPanel.Controls.Add(this.panelPlayBtn); 125 | this.gridPanel.Controls.Add(this.panelRight); 126 | this.gridPanel.Controls.Add(this.centerPanel); 127 | this.gridPanel.Dock = System.Windows.Forms.DockStyle.Fill; 128 | this.gridPanel.Location = new System.Drawing.Point(0, 0); 129 | this.gridPanel.Name = "gridPanel"; 130 | this.gridPanel.RenderInvalidControls = false; 131 | this.gridPanel.Size = new System.Drawing.Size(591, 294); 132 | this.gridPanel.TabIndex = 10; 133 | // 134 | // panelPlayBtn 135 | // 136 | this.panelPlayBtn.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(236)))), ((int)(((byte)(26)))), ((int)(((byte)(85))))); 137 | this.panelPlayBtn.Controls.Add(this.picPlayPause); 138 | this.panelPlayBtn.Cursor = System.Windows.Forms.Cursors.Hand; 139 | this.panelPlayBtn.Location = new System.Drawing.Point(12, 78); 140 | this.panelPlayBtn.Name = "panelPlayBtn"; 141 | this.panelPlayBtn.Size = new System.Drawing.Size(64, 64); 142 | this.panelPlayBtn.TabIndex = 5; 143 | this.panelPlayBtn.Tag = "playPause"; 144 | this.panelPlayBtn.Click += new System.EventHandler(this.playPause_Click); 145 | this.panelPlayBtn.MouseEnter += new System.EventHandler(this.panelPlayBtn_MouseEnter); 146 | this.panelPlayBtn.MouseLeave += new System.EventHandler(this.panelPlayBtn_MouseLeave); 147 | this.panelPlayBtn.Resize += new System.EventHandler(this.panelPlayBtn_Resize); 148 | // 149 | // panelRight 150 | // 151 | this.panelRight.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(48)))), ((int)(((byte)(50)))), ((int)(((byte)(64))))); 152 | this.panelRight.Controls.Add(this.lblVol); 153 | this.panelRight.Location = new System.Drawing.Point(507, 78); 154 | this.panelRight.Name = "panelRight"; 155 | this.panelRight.Size = new System.Drawing.Size(64, 64); 156 | this.panelRight.TabIndex = 8; 157 | this.panelRight.Tag = "rightPanel"; 158 | // 159 | // lblVol 160 | // 161 | this.lblVol.BackColor = System.Drawing.Color.Transparent; 162 | this.lblVol.Font = new System.Drawing.Font("Microsoft Sans Serif", 10F); 163 | this.lblVol.ForeColor = System.Drawing.Color.White; 164 | this.lblVol.Location = new System.Drawing.Point(0, 0); 165 | this.lblVol.Name = "lblVol"; 166 | this.lblVol.Size = new System.Drawing.Size(64, 64); 167 | this.lblVol.TabIndex = 4; 168 | this.lblVol.Text = "100%"; 169 | this.lblVol.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; 170 | // 171 | // picPlayPause 172 | // 173 | this.picPlayPause.BackColor = System.Drawing.Color.Transparent; 174 | this.picPlayPause.Cursor = System.Windows.Forms.Cursors.Hand; 175 | this.picPlayPause.Image = global::ListenMoeClient.Properties.Resources.pause; 176 | this.picPlayPause.Location = new System.Drawing.Point(20, 20); 177 | this.picPlayPause.Name = "picPlayPause"; 178 | this.picPlayPause.Size = new System.Drawing.Size(24, 24); 179 | this.picPlayPause.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; 180 | this.picPlayPause.TabIndex = 0; 181 | this.picPlayPause.TabStop = false; 182 | this.picPlayPause.Click += new System.EventHandler(this.playPause_Click); 183 | // 184 | // centerPanel 185 | // 186 | this.centerPanel.Controls.Add(this.coverImage); 187 | this.centerPanel.Controls.Add(this.picFavourite); 188 | this.centerPanel.Location = new System.Drawing.Point(97, 90); 189 | this.centerPanel.Name = "centerPanel"; 190 | this.centerPanel.Size = new System.Drawing.Size(354, 48); 191 | this.centerPanel.TabIndex = 9; 192 | this.centerPanel.Tag = "centerPanel"; 193 | this.centerPanel.Resize += new System.EventHandler(this.centerPanel_Resize); 194 | // 195 | // coverImage 196 | // 197 | this.coverImage.Anchor = System.Windows.Forms.AnchorStyles.None; 198 | this.coverImage.BackColor = System.Drawing.Color.Transparent; 199 | this.coverImage.Cursor = System.Windows.Forms.Cursors.Hand; 200 | this.coverImage.Location = new System.Drawing.Point(312, -11); 201 | this.coverImage.Name = "coverImage"; 202 | this.coverImage.Size = new System.Drawing.Size(64, 64); 203 | this.coverImage.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; 204 | this.coverImage.TabIndex = 8; 205 | this.coverImage.TabStop = false; 206 | this.coverImage.Click += new System.EventHandler(this.coverImage_Click); 207 | // 208 | // picFavourite 209 | // 210 | this.picFavourite.BackColor = System.Drawing.Color.Transparent; 211 | this.picFavourite.Cursor = System.Windows.Forms.Cursors.Hand; 212 | this.picFavourite.Location = new System.Drawing.Point(149, 0); 213 | this.picFavourite.Name = "picFavourite"; 214 | this.picFavourite.Size = new System.Drawing.Size(48, 48); 215 | this.picFavourite.SizeMode = System.Windows.Forms.PictureBoxSizeMode.Zoom; 216 | this.picFavourite.TabIndex = 7; 217 | this.picFavourite.TabStop = false; 218 | this.picFavourite.Visible = false; 219 | this.picFavourite.Click += new System.EventHandler(this.picFavourite_Click); 220 | // 221 | // MainForm 222 | // 223 | this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F); 224 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; 225 | this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(44)))), ((int)(((byte)(46)))), ((int)(((byte)(59))))); 226 | this.ClientSize = new System.Drawing.Size(591, 294); 227 | this.Controls.Add(this.gridPanel); 228 | this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; 229 | this.Name = "MainForm"; 230 | this.StartPosition = System.Windows.Forms.FormStartPosition.Manual; 231 | this.Text = "Listen.moe"; 232 | this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing); 233 | this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Form1_MouseDown); 234 | this.MouseMove += new System.Windows.Forms.MouseEventHandler(this.Form1_MouseMove); 235 | this.MouseUp += new System.Windows.Forms.MouseEventHandler(this.Form1_MouseUp); 236 | this.gridPanel.ResumeLayout(false); 237 | this.panelPlayBtn.ResumeLayout(false); 238 | this.panelRight.ResumeLayout(false); 239 | ((System.ComponentModel.ISupportInitialize)(this.picPlayPause)).EndInit(); 240 | this.centerPanel.ResumeLayout(false); 241 | ((System.ComponentModel.ISupportInitialize)(this.coverImage)).EndInit(); 242 | ((System.ComponentModel.ISupportInitialize)(this.picFavourite)).EndInit(); 243 | this.ResumeLayout(false); 244 | 245 | } 246 | 247 | #endregion 248 | 249 | private BetterPictureBox picPlayPause; 250 | private System.Windows.Forms.Label lblVol; 251 | private System.Windows.Forms.ContextMenu contextMenu1; 252 | private System.Windows.Forms.Panel panelPlayBtn; 253 | private System.Windows.Forms.MenuItem menuItemCopySongInfo; 254 | private BetterPictureBox picFavourite; 255 | private System.Windows.Forms.Panel panelRight; 256 | private System.Windows.Forms.NotifyIcon notifyIcon1; 257 | private System.Windows.Forms.ContextMenu contextMenu2; 258 | private System.Windows.Forms.MenuItem menuItemShow; 259 | private System.Windows.Forms.MenuItem menuItemPlayPause; 260 | private System.Windows.Forms.MenuItem menuItemResetLocation; 261 | private CenterPanel centerPanel; 262 | private CsGrid.GridPanel gridPanel; 263 | private BetterPictureBox coverImage; 264 | private System.Windows.Forms.MenuItem menuItemOptions; 265 | private System.Windows.Forms.MenuItem menuItem_Close; 266 | private System.Windows.Forms.MenuItem menuItemClose2; 267 | } 268 | } 269 | 270 | -------------------------------------------------------------------------------- /MainForm.cs: -------------------------------------------------------------------------------- 1 | using DiscordRPC; 2 | using Microsoft.WindowsAPICodePack.Taskbar; 3 | using System; 4 | using System.Diagnostics; 5 | using System.Drawing; 6 | using System.Drawing.Drawing2D; 7 | using System.Linq; 8 | using System.Net; 9 | using System.Runtime.InteropServices; 10 | using System.Text; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | using System.Windows.Forms; 14 | 15 | namespace ListenMoeClient 16 | { 17 | public partial class MainForm : Form 18 | { 19 | #region Magical form stuff 20 | [DllImport("user32.dll")] 21 | public static extern int SetWindowLong(IntPtr window, int index, int value); 22 | [DllImport("user32.dll")] 23 | public static extern int GetWindowLong(IntPtr window, int index); 24 | const int GWL_EXSTYLE = -20; 25 | const int WS_EX_TOOLWINDOW = 0x00000080; 26 | const int WS_EX_APPWINDOW = 0x00040000; 27 | 28 | Point originalLocation; 29 | Point preMoveCursorLocation; 30 | int cursorLeftDiff, cursorRightDiff, cursorTopDiff, cursorBottomDiff; 31 | bool moving = false; 32 | 33 | //Screen edge snapping 34 | private const int snapDistance = 10; 35 | private bool CloseToEdge(int pos, int edge) => Math.Abs(pos - edge) <= snapDistance; 36 | 37 | private void Form1_MouseDown(object sender, MouseEventArgs e) 38 | { 39 | if (e.Button == MouseButtons.Left) 40 | { 41 | preMoveCursorLocation = Cursor.Position; 42 | originalLocation = this.Location; 43 | moving = true; 44 | 45 | cursorLeftDiff = preMoveCursorLocation.X - this.Left; 46 | cursorRightDiff = this.Right - preMoveCursorLocation.X; 47 | cursorTopDiff = preMoveCursorLocation.Y - this.Top; 48 | cursorBottomDiff = this.Bottom - preMoveCursorLocation.Y; 49 | } 50 | else if (e.Button == MouseButtons.Right) 51 | { 52 | contextMenu1.Show(this, e.Location); 53 | } 54 | } 55 | 56 | private void Form1_MouseMove(object sender, MouseEventArgs e) 57 | { 58 | if (moving) 59 | { 60 | Point cursorDiff = new Point(Cursor.Position.X - preMoveCursorLocation.X, Cursor.Position.Y - preMoveCursorLocation.Y); 61 | Point newLocation = new Point(originalLocation.X + cursorDiff.X, originalLocation.Y + cursorDiff.Y); 62 | 63 | if (RawInput.IsPressed(VirtualKeys.Shift)) 64 | { 65 | this.Location = newLocation; 66 | } 67 | else 68 | { 69 | Screen s = Screen.FromPoint(newLocation); 70 | 71 | bool hSnapped = false; 72 | bool vSnapped = false; 73 | 74 | SnapToRectangle(s.WorkingArea, ref hSnapped, ref vSnapped, newLocation); 75 | SnapToRectangle(s.Bounds, ref hSnapped, ref vSnapped, newLocation); 76 | 77 | int finalX = newLocation.X; 78 | int finalY = newLocation.Y; 79 | if (hSnapped) 80 | finalX = this.Location.X; 81 | if (vSnapped) 82 | finalY = this.Location.Y; 83 | 84 | this.Location = new Point(finalX, finalY); 85 | 86 | Settings.Set(Setting.LocationX, this.Location.X); 87 | Settings.Set(Setting.LocationY, this.Location.Y); 88 | Settings.WriteSettings(); 89 | } 90 | } 91 | } 92 | 93 | private void SnapToRectangle(Rectangle rect, ref bool hSnappedRef, ref bool vSnappedRef, Point newLocation) 94 | { 95 | bool hSnapped, vSnapped; 96 | if ((hSnapped = CloseToEdge(rect.Left, newLocation.X))) this.Left = rect.Left; 97 | if ((vSnapped = CloseToEdge(rect.Top, newLocation.Y))) this.Top = rect.Top; 98 | if (!hSnapped && (hSnapped = CloseToEdge(rect.Right, newLocation.X + this.Width))) this.Left = rect.Right - this.Width; 99 | if (!vSnapped && (vSnapped = CloseToEdge(rect.Bottom, newLocation.Y + this.Height))) this.Top = rect.Bottom - this.Height; 100 | 101 | hSnappedRef |= hSnapped; 102 | vSnappedRef |= vSnapped; 103 | } 104 | 105 | private void Form1_MouseUp(object sender, MouseEventArgs e) 106 | { 107 | if (moving) 108 | moving = false; 109 | } 110 | 111 | #endregion 112 | 113 | WebStreamPlayer player; 114 | SongInfoStream songInfoStream; 115 | 116 | const string JPOP_STREAM = "https://listen.moe/opus"; 117 | const string KPOP_STREAM = "https://listen.moe/kpop/opus"; 118 | const string DISCORD_PRESENCE_CLIENT_ID = "383375119827075072"; 119 | const string CDN_COVER = "https://cdn.listen.moe/covers/"; 120 | const string MUSIC_LINK = "https://listen.moe/music/albums/"; 121 | 122 | Font titleFont; 123 | Font artistFont; 124 | Font volumeFont; 125 | 126 | public FormSettings SettingsForm; 127 | 128 | Sprite favSprite; 129 | Sprite lightFavSprite; 130 | Sprite darkFavSprite; 131 | Sprite fadedFavSprite; 132 | bool heartFav = false; 133 | 134 | private ThumbnailToolBarButton button; 135 | 136 | CancellationTokenSource cts; 137 | CancellationToken ct; 138 | Task updaterTask; 139 | Task renderLoop; 140 | 141 | Rectangle gripRect = new Rectangle(); 142 | Rectangle rightEdgeRect = new Rectangle(); 143 | Rectangle leftEdgeRect = new Rectangle(); 144 | 145 | bool spriteColorInverted = false; //Excluding play/pause icon 146 | bool playPauseInverted = false; 147 | bool currentlyFavoriting = false; 148 | 149 | public DiscordRpcClient client; 150 | public RichPresence presence; 151 | 152 | public void InitDiscordPresence() 153 | { 154 | client = new DiscordRpcClient(DISCORD_PRESENCE_CLIENT_ID, false, -1); 155 | 156 | presence = new RichPresence() 157 | { 158 | Assets = new Assets() 159 | { 160 | LargeImageKey = Settings.Get(Setting.StreamType) == StreamType.Jpop ? "jpop" : "kpop", 161 | LargeImageText = "LISTEN.moe", 162 | SmallImageKey = "play", 163 | }, 164 | Timestamps = new Timestamps() 165 | { 166 | Start = DateTime.UtcNow 167 | } 168 | }; 169 | 170 | client.Initialize(); 171 | } 172 | 173 | public MainForm() 174 | { 175 | InitializeComponent(); 176 | this.MinimumSize = new Size(Settings.DEFAULT_WIDTH, Settings.DEFAULT_HEIGHT); 177 | 178 | SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); 179 | 180 | centerPanel.MouseDown += Form1_MouseDown; 181 | centerPanel.MouseMove += Form1_MouseMove; 182 | centerPanel.MouseUp += Form1_MouseUp; 183 | panelRight.MouseDown += Form1_MouseDown; 184 | panelRight.MouseMove += Form1_MouseMove; 185 | panelRight.MouseUp += Form1_MouseUp; 186 | 187 | contextMenu1.MenuItems.Add(new MenuItem("LISTEN.moe Desktop Client v" + Globals.VERSION.ToString()) { Enabled = false }); 188 | Settings.LoadSettings(); 189 | //Write immediately after loading to flush any new default settings 190 | Settings.WriteSettings(); 191 | 192 | cts = new CancellationTokenSource(); 193 | ct = cts.Token; 194 | #pragma warning disable CS4014 195 | CheckForUpdates(new TaskFactory(TaskScheduler.FromCurrentSynchronizationContext())); 196 | #pragma warning restore CS4014 197 | StartUpdateAutochecker(); 198 | 199 | MouseWheel += Form1_MouseWheel; 200 | this.Icon = Properties.Resources.icon; 201 | 202 | notifyIcon1.ContextMenu = contextMenu2; 203 | notifyIcon1.Icon = Properties.Resources.icon; 204 | 205 | Task.Run(async () => await LoadFavSprite(heartFav)).Wait(); 206 | 207 | if (Settings.Get(Setting.ThumbnailButton)) 208 | { 209 | button = new ThumbnailToolBarButton(Properties.Resources.pause_ico, "Pause"); 210 | button.Click += async (_, __) => await TogglePlayback(); 211 | TaskbarManager.Instance.ThumbnailToolBars.AddButtons(this.Handle, button); 212 | } 213 | 214 | if (Settings.Get(Setting.DiscordPresence)) 215 | InitDiscordPresence(); 216 | 217 | Connect(); 218 | 219 | string stream = Settings.Get(Setting.StreamType) == StreamType.Jpop ? JPOP_STREAM : KPOP_STREAM; 220 | player = new WebStreamPlayer(stream); 221 | player.Play(); 222 | 223 | renderLoop = Task.Run(() => 224 | { 225 | while (!ct.IsCancellationRequested) 226 | { 227 | centerPanel.Invalidate(); 228 | Thread.Sleep(33); 229 | } 230 | }); 231 | 232 | ReloadScale(); 233 | ReloadSettings(); 234 | 235 | SizeChanged += MainForm_SizeChanged; 236 | UpdatePanelExcludedRegions(); 237 | } 238 | 239 | private async Task LoadFavSprite(bool heart) 240 | { 241 | await Task.Run(() => 242 | { 243 | Bitmap spritesheet = heart ? Properties.Resources.heart_sprite : Properties.Resources.fav_sprite; 244 | int frameSize = heart ? 400 : 256; 245 | lightFavSprite = SpriteLoader.LoadFavSprite(spritesheet, frameSize); 246 | fadedFavSprite = SpriteLoader.LoadFadedFavSprite(spritesheet, frameSize); 247 | darkFavSprite = SpriteLoader.LoadDarkFavSprite(spritesheet, frameSize); 248 | favSprite = lightFavSprite; 249 | }); 250 | 251 | picFavourite.ResetScale(); 252 | if (heart) 253 | picFavourite.Size = new Size(48, 48); 254 | else 255 | picFavourite.Size = new Size(32, 32); 256 | 257 | bool favourite = songInfoStream?.currentInfo?.song.favorite ?? false; 258 | picFavourite.Image = favourite ? favSprite.Frames[favSprite.Frames.Length - 1] : favSprite.Frames[0]; 259 | 260 | ReloadScale(); 261 | } 262 | 263 | protected override void OnPaint(PaintEventArgs e) 264 | { 265 | base.OnPaint(e); 266 | 267 | e.Graphics.InterpolationMode = InterpolationMode.NearestNeighbor; 268 | e.Graphics.PixelOffsetMode = PixelOffsetMode.Half; 269 | e.Graphics.DrawImage(spriteColorInverted ? Properties.Resources.gripper_inverted : Properties.Resources.gripper, gripRect); 270 | 271 | //Expose 2px on the left for resizing, so we paint it the same colour so it's not noticeable 272 | Color baseAccentColor = Settings.Get(Setting.StreamType) == StreamType.Jpop ? Settings.Get(Setting.JPOPAccentColor) : Settings.Get(Setting.KPOPAccentColor); 273 | Color accentColor = Settings.Get(Setting.CustomColors) ? Settings.Get(Setting.CustomAccentColor) : baseAccentColor; 274 | e.Graphics.FillRectangle(new SolidBrush(accentColor), new Rectangle(0, 0, 2, this.ClientRectangle.Height)); 275 | } 276 | 277 | private void UpdatePanelExcludedRegions() 278 | { 279 | int width = this.ClientRectangle.Width; 280 | int height = this.ClientRectangle.Height; 281 | GraphicsPath path = new GraphicsPath(); 282 | 283 | float scale = Settings.Get(Setting.Scale); 284 | int gripSize = (int)(Properties.Resources.gripper.Width * scale); 285 | int padding = (int)scale; 286 | 287 | path.AddPolygon(new[] { new Point(width - gripSize * 2 - padding, height), new Point(width, height - gripSize * 2 - padding), new Point(width, height) }); 288 | gripRect = new Rectangle(width - gripSize - padding, height - gripSize - padding, gripSize, gripSize); 289 | rightEdgeRect = new Rectangle(width - 2, 0, 2, height); 290 | leftEdgeRect = new Rectangle(0, 0, 2, height); 291 | 292 | Region region = new Region(new Rectangle(0, 0, width, height)); 293 | region.Exclude(path); 294 | region.Exclude(rightEdgeRect); 295 | region.Exclude(leftEdgeRect); 296 | gridPanel.Region = region; 297 | panelRight.Region = region; 298 | } 299 | 300 | private void MainForm_SizeChanged(object sender, EventArgs e) 301 | { 302 | UpdatePanelExcludedRegions(); 303 | centerPanel.ReloadVisualiser(); 304 | Invalidate(); 305 | 306 | //wow such performance 307 | //TODO: don't make this write to disk on every resize event 308 | //Settings buffering would be nice 309 | Settings.Set(Setting.SizeX, this.Width); 310 | Settings.Set(Setting.SizeY, this.Height); 311 | Settings.WriteSettings(); 312 | } 313 | 314 | protected override void WndProc(ref Message m) 315 | { 316 | WM message = (WM)m.Msg; 317 | if (message == WM.INPUT) 318 | RawInput.ProcessMessage(m.LParam); 319 | else if (message == WM.NCHITTEST) 320 | { 321 | Point pos = new Point(m.LParam.ToInt32()); 322 | pos = PointToClient(pos); 323 | if (gripRect.Contains(pos)) 324 | m.Result = (IntPtr)17; 325 | else if (rightEdgeRect.Contains(pos)) 326 | m.Result = (IntPtr)11; 327 | else if (leftEdgeRect.Contains(pos)) 328 | m.Result = (IntPtr)10; 329 | return; 330 | } 331 | if (m.Msg == Program.WM_SHOWME) 332 | { 333 | Restore(); 334 | } 335 | base.WndProc(ref m); 336 | } 337 | 338 | public void ReloadScale() 339 | { 340 | this.Width = Settings.DEFAULT_WIDTH; 341 | this.Height = Settings.DEFAULT_HEIGHT; 342 | float scaleFactor = Settings.Get(Setting.Scale); 343 | this.BetterScale(scaleFactor); 344 | 345 | gridPanel.SetRows("100%"); 346 | int playPauseWidth = (int)(Settings.DEFAULT_HEIGHT * scaleFactor); 347 | int rightPanelWidth = (int)(Settings.DEFAULT_RIGHT_PANEL_WIDTH * scaleFactor); 348 | gridPanel.SetColumns($"{playPauseWidth}px auto {rightPanelWidth}px"); 349 | gridPanel.DefineAreas("playPause centerPanel rightPanel"); 350 | 351 | //Reload fonts to get newly scaled font sizes 352 | LoadFonts(); 353 | SetPlayPauseSize(false); 354 | } 355 | 356 | public void ReloadSettings() 357 | { 358 | this.TopMost = Settings.Get(Setting.TopMost); 359 | 360 | this.Location = new Point(Settings.Get(Setting.LocationX), Settings.Get(Setting.LocationY)); 361 | this.Size = new Size(Settings.Get(Setting.SizeX), Settings.Get(Setting.SizeY)); 362 | 363 | if (Settings.Get(Setting.EnableVisualiser)) 364 | centerPanel.StartVisualiser(player); 365 | else 366 | centerPanel.StopVisualiser(player); 367 | centerPanel.ReloadVisualiser(); 368 | 369 | float vol = Settings.Get(Setting.Volume); 370 | Color baseAccentColor = Settings.Get(Setting.StreamType) == StreamType.Jpop ? Settings.Get(Setting.JPOPAccentColor) : Settings.Get(Setting.KPOPAccentColor); 371 | Color accentColor = Settings.Get(Setting.CustomColors) ? Settings.Get(Setting.CustomAccentColor) : baseAccentColor; 372 | panelPlayBtn.BackColor = accentColor; 373 | playPauseInverted = accentColor.R + accentColor.G + accentColor.B > 128 * 3; 374 | 375 | Color baseColor = Settings.Get(Setting.StreamType) == StreamType.Jpop ? Settings.Get(Setting.JPOPBaseColor) : Settings.Get(Setting.KPOPBaseColor); 376 | Color color = Settings.Get(Setting.CustomColors) ? Settings.Get(Setting.CustomBaseColor) : baseColor; 377 | spriteColorInverted = color.R + color.G + color.B > 128 * 3; 378 | centerPanel.BackColor = color; 379 | panelRight.BackColor = color.Scale(1.3f); 380 | 381 | //Set form colour to panel right colour so the correct colour shines through during region exclusion 382 | this.BackColor = panelRight.BackColor; 383 | ReloadSprites(); 384 | 385 | SetVolumeLabel(vol); 386 | this.Opacity = Settings.Get(Setting.FormOpacity); 387 | 388 | if (Settings.Get(Setting.HideFromAltTab)) 389 | { 390 | this.ShowInTaskbar = false; 391 | int windowStyle = GetWindowLong(this.Handle, GWL_EXSTYLE); 392 | SetWindowLong(this.Handle, GWL_EXSTYLE, windowStyle | WS_EX_TOOLWINDOW); 393 | notifyIcon1.Visible = true; 394 | } 395 | else 396 | { 397 | this.ShowInTaskbar = true; 398 | int windowStyle = GetWindowLong(this.Handle, GWL_EXSTYLE); 399 | SetWindowLong(this.Handle, GWL_EXSTYLE, windowStyle & ~WS_EX_TOOLWINDOW); 400 | notifyIcon1.Visible = false; 401 | } 402 | 403 | RawInput.RegisterDevice(HIDUsagePage.Generic, HIDUsage.Keyboard, RawInputDeviceFlags.InputSink, this.Handle); 404 | RawInput.RegisterCallback(VirtualKeys.MediaPlayPause, async () => await TogglePlayback()); 405 | RawInput.RegisterPassword(new[] { 406 | VirtualKeys.Up, VirtualKeys.Up, 407 | VirtualKeys.Down, VirtualKeys.Down, 408 | VirtualKeys.Left, VirtualKeys.Right, 409 | VirtualKeys.Left, VirtualKeys.Right, 410 | VirtualKeys.B, VirtualKeys.A, 411 | VirtualKeys.Return 412 | }, async () => await LoadFavSprite(heartFav = !heartFav)); 413 | Invalidate(); 414 | } 415 | 416 | public async Task ReloadStream() 417 | { 418 | centerPanel.StopVisualiser(player); 419 | 420 | // Reload audio stream 421 | await player.Stop(); 422 | string stream = Settings.Get(Setting.StreamType) == StreamType.Jpop ? JPOP_STREAM : KPOP_STREAM; 423 | player.SetStreamUrl(stream); 424 | player.Play(); 425 | 426 | // Reload web socket 427 | songInfoStream.Reconnect(); 428 | centerPanel.ReloadVisualiser(); 429 | 430 | if (Settings.Get(Setting.EnableVisualiser)) 431 | centerPanel.StartVisualiser(player); 432 | } 433 | 434 | private void LoadFonts() 435 | { 436 | float scaleFactor = Settings.Get(Setting.Scale); 437 | titleFont = Meiryo.GetFont(13 * scaleFactor); 438 | artistFont = Meiryo.GetFont(8 * scaleFactor); 439 | volumeFont = Meiryo.GetFont(8 * scaleFactor); 440 | 441 | lblVol.Font = volumeFont; 442 | 443 | centerPanel.SetFonts(titleFont, artistFont); 444 | } 445 | 446 | private async void Connect() 447 | { 448 | TaskScheduler scheduler = TaskScheduler.FromCurrentSynchronizationContext(); 449 | await LoadWebSocket(scheduler); 450 | } 451 | 452 | private async Task LoadWebSocket(TaskScheduler scheduler) 453 | { 454 | await Task.Run(async () => 455 | { 456 | TaskFactory factory = new TaskFactory(scheduler); 457 | songInfoStream = new SongInfoStream(factory); 458 | songInfoStream.OnSongInfoReceived += ProcessSongInfo; 459 | 460 | User.OnLoginComplete += () => 461 | { 462 | factory.StartNew(() => picFavourite.Visible = true); 463 | songInfoStream.Authenticate(); 464 | }; 465 | User.OnLogout += async () => 466 | { 467 | await factory.StartNew(() => picFavourite.Visible = false); 468 | await Task.Run(() => songInfoStream.Reconnect()); 469 | }; 470 | string savedToken = Settings.Get(Setting.Token).Trim(); 471 | if (savedToken != "") 472 | await User.Login(savedToken); 473 | }); 474 | } 475 | 476 | private async Task CheckForUpdates(TaskFactory factory) 477 | { 478 | if (await Updater.CheckGithubVersion()) 479 | { 480 | System.Media.SystemSounds.Beep.Play(); //DING 481 | DialogResult result = await factory.StartNew(() => MessageBox.Show(this, "An update is available for the Listen.moe player. Do you want to update and restart the application now?", 482 | "Listen.moe client - Update available - current version " + Globals.VERSION, MessageBoxButtons.YesNo)); 483 | if (result == DialogResult.Yes) 484 | { 485 | centerPanel.SetUpdateState(UpdateState.InProgress); 486 | await Updater.UpdateToNewVersion(Wc_DownloadProgressChanged, Wc_DownloadFileCompleted); 487 | } 488 | } 489 | } 490 | 491 | private void StartUpdateAutochecker() 492 | { 493 | TaskFactory factory = new TaskFactory(TaskScheduler.FromCurrentSynchronizationContext()); 494 | updaterTask = Task.Run(async () => 495 | { 496 | Stopwatch stopwatch = new Stopwatch(); 497 | stopwatch.Start(); 498 | long last = stopwatch.ElapsedMilliseconds; 499 | while (!ct.IsCancellationRequested) 500 | { 501 | if (stopwatch.ElapsedMilliseconds - last > Settings.Get(Setting.UpdateInterval) * 1000) 502 | { 503 | //We only check for the setting here, because I'm lazy to dispose/recreate this checker thread when they change the setting 504 | if (!Settings.Get(Setting.UpdateAutocheck)) 505 | { 506 | last = stopwatch.ElapsedMilliseconds; 507 | continue; 508 | } 509 | 510 | await CheckForUpdates(factory); 511 | last = stopwatch.ElapsedMilliseconds; 512 | } 513 | else 514 | { 515 | Thread.Sleep(100); 516 | } 517 | } 518 | }); 519 | } 520 | 521 | private void Wc_DownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e) => centerPanel.SetUpdateState(UpdateState.Complete); 522 | 523 | private void Wc_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e) => centerPanel.SetUpdatePercent(e.BytesReceived / (float)e.TotalBytesToReceive); 524 | 525 | private void Form1_MouseWheel(object sender, MouseEventArgs e) 526 | { 527 | if (e.Delta != 0) 528 | { 529 | if (RawInput.IsPressed(VirtualKeys.Control)) 530 | { 531 | centerPanel.Visualiser.IncreaseBarWidth(0.5f * e.Delta / SystemInformation.MouseWheelScrollDelta); 532 | } 533 | else if (RawInput.IsPressed(VirtualKeys.Menu)) 534 | { 535 | centerPanel.Visualiser.IncreaseResolution(e.Delta / SystemInformation.MouseWheelScrollDelta); 536 | } 537 | else 538 | { 539 | float delta = 0.05f; 540 | if (RawInput.IsPressed(VirtualKeys.Shift)) 541 | delta = 0.01f; 542 | float volumeChange = (e.Delta / (float)SystemInformation.MouseWheelScrollDelta) * delta; 543 | float newVol = player.AddVolume(volumeChange); 544 | if (newVol >= 0) 545 | { 546 | Settings.Set(Setting.Volume, newVol); 547 | Settings.WriteSettings(); 548 | SetVolumeLabel(newVol); 549 | } 550 | } 551 | } 552 | } 553 | 554 | private void SetVolumeLabel(float vol) 555 | { 556 | int newVol = (int)Math.Round(vol * 100); 557 | lblVol.Text = newVol.ToString() + "%"; 558 | } 559 | 560 | private async void playPause_Click(object sender, EventArgs e) => await TogglePlayback(); 561 | 562 | private void ReloadSprites() 563 | { 564 | if (spriteColorInverted) 565 | { 566 | centerPanel.SetLabelBrush(Brushes.Black); 567 | lblVol.ForeColor = Color.Black; 568 | 569 | favSprite = darkFavSprite; 570 | } 571 | else 572 | { 573 | centerPanel.SetLabelBrush(Brushes.White); 574 | lblVol.ForeColor = Color.White; 575 | favSprite = lightFavSprite; 576 | } 577 | 578 | if (playPauseInverted) 579 | { 580 | if (player.IsPlaying()) 581 | picPlayPause.Image = Properties.Resources.pause_inverted; 582 | else 583 | picPlayPause.Image = Properties.Resources.play; 584 | } 585 | else 586 | { 587 | if (player.IsPlaying()) 588 | picPlayPause.Image = Properties.Resources.pause; 589 | else 590 | picPlayPause.Image = Properties.Resources.play; 591 | } 592 | 593 | if (songInfoStream?.currentInfo?.song.favorite ?? false) 594 | picFavourite.Image = favSprite?.Frames[favSprite.Frames.Length - 1]; 595 | else 596 | picFavourite.Image = favSprite?.Frames[0]; 597 | } 598 | 599 | private async Task TogglePlayback() 600 | { 601 | if (player.IsPlaying()) 602 | { 603 | Task stopTask = player.Stop(); 604 | ReloadSprites(); 605 | menuItemPlayPause.Text = "Play"; 606 | if (Settings.Get(Setting.ThumbnailButton) && !Settings.Get(Setting.HideFromAltTab)) 607 | { 608 | button.Icon = Properties.Resources.play_ico; 609 | button.Tooltip = "Play"; 610 | } 611 | if (Settings.Get(Setting.EnableVisualiser)) 612 | centerPanel.StopVisualiser(player); 613 | await stopTask; 614 | } 615 | else 616 | { 617 | player.Play(); 618 | ReloadSprites(); 619 | menuItemPlayPause.Text = "Pause"; 620 | if (Settings.Get(Setting.ThumbnailButton) && !Settings.Get(Setting.HideFromAltTab)) 621 | { 622 | button.Icon = Properties.Resources.pause_ico; 623 | button.Tooltip = "Pause"; 624 | } 625 | if (Settings.Get(Setting.EnableVisualiser)) 626 | centerPanel.StartVisualiser(player); 627 | } 628 | } 629 | 630 | void ProcessSongInfo(SongInfoResponseData songInfo) 631 | { 632 | string eventInfo = songInfo.requester != null ? "Requested by " + songInfo.requester.displayName : songInfo._event?.name ?? ""; 633 | string sources = string.Join(", ", songInfo.song.sources.Select(s => 634 | { 635 | if (!string.IsNullOrWhiteSpace(s.nameRomaji)) 636 | return s.nameRomaji; 637 | return s.name; 638 | })); 639 | string artists = string.Join(", ", songInfo.song.artists.Select(a => 640 | { 641 | if (!string.IsNullOrWhiteSpace(a.nameRomaji)) 642 | return a.nameRomaji; 643 | return a.name; 644 | })); 645 | centerPanel.SetLabelText(songInfo.song.title, artists, sources, eventInfo, !string.IsNullOrWhiteSpace(eventInfo)); 646 | 647 | StreamType type = Settings.Get(Setting.StreamType); 648 | presence.Details = songInfo.song.title.Length >= 50 ? songInfo.song.title.Substring(0, 50) : songInfo.song.title; 649 | presence.State = artists.Length >= 50 ? "by " + artists.Substring(0, 50) : "by " + artists; 650 | presence.Timestamps.Start = DateTime.UtcNow; 651 | presence.Timestamps.End = songInfo.startTime.AddMilliseconds(songInfo.song.duration * 1000); 652 | presence.Assets.LargeImageText = type == StreamType.Jpop ? "LISTEN.moe - JPOP" : "LISTEN.moe - KPOP"; 653 | if (songInfo._event != null) 654 | { 655 | presence.Assets.LargeImageKey = Convert.ToBoolean(songInfo._event.presence) ? songInfo._event.presence : type == StreamType.Jpop ? "jpop" : "kpop"; 656 | } else 657 | { 658 | presence.Assets.LargeImageKey = type == StreamType.Jpop ? "jpop" : "kpop"; 659 | } 660 | 661 | if (songInfo.song.albums.Length != 0 && songInfo.song.albums[0].image != null) 662 | { 663 | coverImage.Load(CDN_COVER + songInfo.song.albums[0].image); 664 | coverImage.Visible = true; 665 | } 666 | else 667 | { 668 | coverImage.Visible = false; 669 | } 670 | 671 | if (User.LoggedIn) 672 | { 673 | SetFavouriteSprite(songInfo.song.favorite); 674 | centerPanel_Resize(null, null); 675 | } 676 | else 677 | { 678 | picFavourite.Visible = false; 679 | } 680 | 681 | if (Settings.Get(Setting.DiscordPresence)) 682 | { 683 | client.SetPresence(presence); 684 | client.Invoke(); 685 | } 686 | } 687 | 688 | private async void Form1_FormClosing(object sender, FormClosingEventArgs e) => await Exit(); 689 | 690 | private async Task Exit() 691 | { 692 | cts.Cancel(); 693 | if (SettingsForm != null) 694 | { 695 | SettingsForm.Close(); 696 | } 697 | Hide(); 698 | notifyIcon1.Visible = false; 699 | await player.Dispose(); 700 | client.Dispose(); 701 | Environment.Exit(0); 702 | } 703 | 704 | private void panelPlayBtn_MouseEnter(object sender, EventArgs e) => SetPlayPauseSize(true); 705 | 706 | private void panelPlayBtn_MouseLeave(object sender, EventArgs e) 707 | { 708 | if (panelPlayBtn.ClientRectangle.Contains(PointToClient(MousePosition))) 709 | return; 710 | SetPlayPauseSize(false); 711 | } 712 | 713 | private void SetPlayPauseSize(bool bigger) 714 | { 715 | float scale = Settings.Get(Setting.Scale); 716 | int ppSize = Settings.DEFAULT_PLAY_PAUSE_SIZE; 717 | int playPauseSize = bigger ? ppSize + 2 : ppSize; 718 | 719 | picPlayPause.Size = new Size((int)(playPauseSize * scale), (int)(playPauseSize * scale)); 720 | int y = (panelPlayBtn.Height / 2) - (picPlayPause.Height / 2); 721 | int x = (panelPlayBtn.Width / 2) - (picPlayPause.Width / 2); 722 | picPlayPause.Location = new Point(x, y); 723 | } 724 | 725 | private void menuItemCopySongInfo_Click(object sender, EventArgs e) 726 | { 727 | SongInfoResponseData info = songInfoStream.currentInfo; 728 | if (info != null) 729 | { 730 | StringBuilder sb = new StringBuilder(); 731 | sb.AppendLine(string.Join(", ", info.song.artists.Select(a => { 732 | if (!string.IsNullOrWhiteSpace(a.nameRomaji)) 733 | return a.nameRomaji; 734 | return a.name; 735 | })) + " - " + info.song.title + (info.song.sources.Length != 0 ? " [" + string.Join(", ", info.song.sources.Select(s => 736 | { 737 | if (!string.IsNullOrWhiteSpace(s.nameRomaji)) 738 | return s.nameRomaji; 739 | return s.name; 740 | })) 741 | + "]" : "")); 742 | Clipboard.SetText(sb.ToString()); 743 | } 744 | } 745 | 746 | private async void menuItemClose_Click(object sender, EventArgs e) 747 | { 748 | if (Settings.Get(Setting.CloseToTray)) 749 | { 750 | if (!Settings.Get(Setting.HideFromAltTab)) 751 | notifyIcon1.Visible = true; 752 | 753 | Hide(); 754 | } 755 | else 756 | { 757 | Close(); 758 | await Exit(); 759 | } 760 | } 761 | 762 | private void notifyIcon1_MouseDoubleClick(object sender, MouseEventArgs e) 763 | { 764 | if (e.Button == MouseButtons.Left) 765 | Restore(); 766 | } 767 | 768 | private void menuItemShow_Click(object sender, EventArgs e) => Restore(); 769 | 770 | private void Restore() 771 | { 772 | this.WindowState = FormWindowState.Normal; 773 | Show(); 774 | Activate(); 775 | 776 | if (!Settings.Get(Setting.HideFromAltTab)) 777 | notifyIcon1.Visible = false; 778 | } 779 | 780 | int currentFrame = 0; 781 | bool isAnimating = false; 782 | 783 | object animationLock = new object(); 784 | 785 | private void panelPlayBtn_Resize(object sender, EventArgs e) => SetPlayPauseSize(false); 786 | 787 | private void centerPanel_Resize(object sender, EventArgs e) 788 | { 789 | float scale = Settings.Get(Setting.Scale); 790 | if (!coverImage.Visible) 791 | picFavourite.Location = new Point(centerPanel.Width - (picFavourite.Width), (centerPanel.Height / 2) - (picFavourite.Height / 2)); 792 | else 793 | picFavourite.Location = new Point(centerPanel.Width - (picFavourite.Width + coverImage.Width), (centerPanel.Height / 2) - (picFavourite.Height / 2)); 794 | coverImage.Location = new Point(centerPanel.Width - coverImage.Width, (centerPanel.Height / 2) - (coverImage.Height / 2)); 795 | } 796 | 797 | private void coverImage_Click(object sender, EventArgs e) 798 | { 799 | SongInfoResponseData info = songInfoStream.currentInfo; 800 | if (info != null) 801 | if (info.song.albums[0].image != null) 802 | Process.Start(MUSIC_LINK + songInfoStream.currentInfo.song.albums[0].id); 803 | } 804 | 805 | private void menuItemOptions_Click(object sender, EventArgs e) 806 | { 807 | if (SettingsForm == null) 808 | { 809 | SettingsForm = new FormSettings(this, player.BasePlayer) 810 | { 811 | StartPosition = FormStartPosition.CenterScreen 812 | }; 813 | SettingsForm.Show(); 814 | } 815 | else 816 | { 817 | SettingsForm.Activate(); 818 | } 819 | } 820 | 821 | private async void menuItemClose2_Click(object sender, EventArgs e) => await Exit(); 822 | 823 | private async void SetFavouriteSprite(bool favourited) 824 | { 825 | await Task.Run(() => 826 | { 827 | while (favSprite == null) 828 | { 829 | 830 | } 831 | }); 832 | picFavourite.Visible = true; 833 | if (favourited) 834 | { 835 | lock (animationLock) 836 | { 837 | currentFrame = 0; 838 | //Reset animation and exit 839 | if (isAnimating) 840 | return; 841 | isAnimating = true; 842 | } 843 | 844 | //Animate. 845 | while (currentFrame < favSprite.Frames.Length) 846 | { 847 | lock (animationLock) 848 | { 849 | if (!isAnimating) 850 | break; 851 | } 852 | picFavourite.Image = favSprite.Frames[currentFrame++]; 853 | await Task.Delay(16); 854 | } 855 | 856 | isAnimating = false; 857 | } 858 | else 859 | { 860 | lock (animationLock) 861 | isAnimating = false; 862 | picFavourite.Image = favSprite.Frames[0]; 863 | } 864 | } 865 | 866 | private async void picFavourite_Click(object sender, EventArgs e) 867 | { 868 | if (songInfoStream.currentInfo == null || songInfoStream.currentInfo.song == null) 869 | return; 870 | 871 | bool currentStatus = songInfoStream.currentInfo.song.favorite; 872 | bool newStatus = !currentStatus; 873 | 874 | SetFavouriteSprite(newStatus); 875 | 876 | if (currentlyFavoriting) 877 | return; 878 | 879 | currentlyFavoriting = true; 880 | 881 | string id = songInfoStream.currentInfo.song.id.ToString(); 882 | 883 | bool success = await User.FavoriteSong(id, newStatus); 884 | bool finalState = success ? newStatus : currentStatus; 885 | 886 | picFavourite.Image = finalState ? favSprite.Frames[favSprite.Frames.Length - 1] : 887 | spriteColorInverted ? darkFavSprite.Frames[0] : favSprite.Frames[0]; 888 | songInfoStream.currentInfo.song.favorite = finalState; 889 | 890 | currentlyFavoriting = false; 891 | } 892 | 893 | private void menuItemResetLocation_Click(object sender, EventArgs e) 894 | { 895 | Settings.Set(Setting.LocationX, 0); 896 | Settings.Set(Setting.LocationY, 0); 897 | Settings.WriteSettings(); 898 | this.Location = new Point(0, 0); 899 | } 900 | } 901 | } 902 | -------------------------------------------------------------------------------- /MainForm.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 | 17, 17 122 | 123 | 124 | 147, 17 125 | 126 | 127 | 260, 17 128 | 129 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Runtime.InteropServices; 4 | using System.Threading; 5 | using System.Windows.Forms; 6 | 7 | namespace ListenMoeClient 8 | { 9 | static class Program 10 | { 11 | static Mutex mutex = new Mutex(true, "{6431a734-2693-40d4-8dff-ea662d8777d7}"); 12 | 13 | public const int HWND_BROADCAST = 0xffff; 14 | public static readonly uint WM_SHOWME = RegisterWindowMessage("WM_SHOWME"); 15 | [DllImport("user32")] 16 | static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); 17 | [DllImport("user32")] 18 | static extern uint RegisterWindowMessage(string lpString); 19 | 20 | /// 21 | /// The main entry point for the application. 22 | /// 23 | [STAThread] 24 | static void Main() 25 | { 26 | if (mutex.WaitOne(TimeSpan.Zero, true)) 27 | { 28 | ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; 29 | Environment.SetEnvironmentVariable("LANG", "ja_JP.utf-8"); 30 | Application.EnableVisualStyles(); 31 | Application.SetCompatibleTextRenderingDefault(false); 32 | Application.Run(new MainForm()); 33 | } 34 | else 35 | { 36 | SendMessage((IntPtr)HWND_BROADCAST, WM_SHOWME, IntPtr.Zero, IntPtr.Zero); 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Listen.moe Client")] 8 | [assembly: AssemblyProduct("Listen.moe Client")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyCopyright("Copyright 2017-2018 Listen.moe. All rights reserved.")] 11 | 12 | // Setting ComVisible to false makes the types in this assembly not visible 13 | // to COM components. If you need to access a type in this assembly from 14 | // COM, set the ComVisible attribute to true on that type. 15 | [assembly: ComVisible(false)] 16 | 17 | // The following GUID is for the ID of the typelib if this project is exposed to COM 18 | [assembly: Guid("88b02799-425e-4622-a849-202adb19601b")] 19 | 20 | // Version information for an assembly consists of the following four values: 21 | // 22 | // Major Version 23 | // Minor Version 24 | // Build Number 25 | // Revision 26 | // 27 | // You can specify all the values or you can default the Build and Revision Numbers 28 | // by using the '*' as shown below: 29 | // [assembly: AssemblyVersion("1.0.*")] 30 | [assembly: AssemblyVersion("2.0.0.0")] 31 | [assembly: AssemblyFileVersion("2.0.0.0")] 32 | -------------------------------------------------------------------------------- /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 ListenMoeClient.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", "15.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("ListenMoeClient.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 resource of type System.Drawing.Bitmap. 65 | /// 66 | internal static System.Drawing.Bitmap close_inverted { 67 | get { 68 | object obj = ResourceManager.GetObject("close_inverted", resourceCulture); 69 | return ((System.Drawing.Bitmap)(obj)); 70 | } 71 | } 72 | 73 | /// 74 | /// Looks up a localized resource of type System.Drawing.Bitmap. 75 | /// 76 | internal static System.Drawing.Bitmap cog_inverted { 77 | get { 78 | object obj = ResourceManager.GetObject("cog_inverted", resourceCulture); 79 | return ((System.Drawing.Bitmap)(obj)); 80 | } 81 | } 82 | 83 | /// 84 | /// Looks up a localized resource of type System.Drawing.Bitmap. 85 | /// 86 | internal static System.Drawing.Bitmap fav_sprite { 87 | get { 88 | object obj = ResourceManager.GetObject("fav_sprite", resourceCulture); 89 | return ((System.Drawing.Bitmap)(obj)); 90 | } 91 | } 92 | 93 | /// 94 | /// Looks up a localized resource of type System.Drawing.Bitmap. 95 | /// 96 | internal static System.Drawing.Bitmap gripper { 97 | get { 98 | object obj = ResourceManager.GetObject("gripper", resourceCulture); 99 | return ((System.Drawing.Bitmap)(obj)); 100 | } 101 | } 102 | 103 | /// 104 | /// Looks up a localized resource of type System.Drawing.Bitmap. 105 | /// 106 | internal static System.Drawing.Bitmap gripper_inverted { 107 | get { 108 | object obj = ResourceManager.GetObject("gripper_inverted", resourceCulture); 109 | return ((System.Drawing.Bitmap)(obj)); 110 | } 111 | } 112 | 113 | /// 114 | /// Looks up a localized resource of type System.Drawing.Bitmap. 115 | /// 116 | internal static System.Drawing.Bitmap heart_sprite { 117 | get { 118 | object obj = ResourceManager.GetObject("heart_sprite", resourceCulture); 119 | return ((System.Drawing.Bitmap)(obj)); 120 | } 121 | } 122 | 123 | /// 124 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 125 | /// 126 | internal static System.Drawing.Icon icon { 127 | get { 128 | object obj = ResourceManager.GetObject("icon", resourceCulture); 129 | return ((System.Drawing.Icon)(obj)); 130 | } 131 | } 132 | 133 | /// 134 | /// Looks up a localized resource of type System.Byte[]. 135 | /// 136 | internal static byte[] Meiryo { 137 | get { 138 | object obj = ResourceManager.GetObject("Meiryo", resourceCulture); 139 | return ((byte[])(obj)); 140 | } 141 | } 142 | 143 | /// 144 | /// Looks up a localized resource of type System.Drawing.Bitmap. 145 | /// 146 | internal static System.Drawing.Bitmap pause { 147 | get { 148 | object obj = ResourceManager.GetObject("pause", resourceCulture); 149 | return ((System.Drawing.Bitmap)(obj)); 150 | } 151 | } 152 | 153 | /// 154 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 155 | /// 156 | internal static System.Drawing.Icon pause_ico { 157 | get { 158 | object obj = ResourceManager.GetObject("pause_ico", resourceCulture); 159 | return ((System.Drawing.Icon)(obj)); 160 | } 161 | } 162 | 163 | /// 164 | /// Looks up a localized resource of type System.Drawing.Bitmap. 165 | /// 166 | internal static System.Drawing.Bitmap pause_inverted { 167 | get { 168 | object obj = ResourceManager.GetObject("pause_inverted", resourceCulture); 169 | return ((System.Drawing.Bitmap)(obj)); 170 | } 171 | } 172 | 173 | /// 174 | /// Looks up a localized resource of type System.Drawing.Bitmap. 175 | /// 176 | internal static System.Drawing.Bitmap play { 177 | get { 178 | object obj = ResourceManager.GetObject("play", resourceCulture); 179 | return ((System.Drawing.Bitmap)(obj)); 180 | } 181 | } 182 | 183 | /// 184 | /// Looks up a localized resource of type System.Drawing.Icon similar to (Icon). 185 | /// 186 | internal static System.Drawing.Icon play_ico { 187 | get { 188 | object obj = ResourceManager.GetObject("play_ico", resourceCulture); 189 | return ((System.Drawing.Icon)(obj)); 190 | } 191 | } 192 | 193 | /// 194 | /// Looks up a localized resource of type System.Drawing.Bitmap. 195 | /// 196 | internal static System.Drawing.Bitmap play_inverted { 197 | get { 198 | object obj = ResourceManager.GetObject("play_inverted", resourceCulture); 199 | return ((System.Drawing.Bitmap)(obj)); 200 | } 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /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 | 122 | ..\Resources\fav_sprite.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 123 | 124 | 125 | ..\Resources\pause.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 126 | 127 | 128 | ..\Resources\play.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 129 | 130 | 131 | ..\Resources\Meiryo.ttf;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 132 | 133 | 134 | ..\Resources\icon.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 135 | 136 | 137 | ..\Resources\pause_ico.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 138 | 139 | 140 | ..\Resources\play_ico.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 141 | 142 | 143 | ..\Resources\pause_inverted.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 144 | 145 | 146 | ..\Resources\play_inverted.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 147 | 148 | 149 | ..\Resources\close_inverted.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 150 | 151 | 152 | ..\Resources\cog_inverted.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 153 | 154 | 155 | ..\Resources\gripper.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 156 | 157 | 158 | ..\Resources\gripper-inverted.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 159 | 160 | 161 | ..\Resources\heart_sprite.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 162 | 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Deprecated. Please use https://github.com/LISTEN-moe/desktop-app 2 | 3 |
4 | 5 |
6 |

Official Windows Client

7 |

Simple lightweight LISTEN.moe client for Windows.

8 | 9 | ![](https://i.imgur.com/vtEKxw2.gif) 10 | 11 | The window will snap to the edges of your screen, and you can mouse scroll on the window to increase/decrease the volume. You can also set it to be topmost by right clicking the window and selecting 'Always on top'. 12 | 13 | # Instructions 14 | 15 | ## Installation 16 | Download the latest release from [here](https://github.com/anonymousthing/ListenMoeClient/releases) and run it. 17 | 18 | ## Updates 19 | Auto-updates are baked into the app (updates are checked on startup only though), which will check for updates, download the latest version and restart the app automatically for you if you click OK. It backs up your older version to `ListenMoe.bak` in the same folder. You can disable update checking on startup by enabling "Ignore updates" in the settings screen. 20 | If you have disabled updates and find that music no longer plays/song info no longer updates, you may want to check for updates; we may have changed the stream/song info API. 21 | 22 | ## Hide from alt-tab 23 | If you would like to hide the program from your alt-tab menu, open `listenMoeSettings.ini` and change the line `bHideFromAltTab=False` to `bHideFromAltTab=True`. A side effect of hiding it from the alt-tab menu is that it will no longer appear in your taskbar either. As such, if this setting is enabled, the System Tray icon will be visible at all times regardless of the "Close to Tray" option. I do not currently have a UI setting for this, as an unsuspecting user may enable it and then "lose" the app underneath many other windows (and not notice the System Tray icon). 24 | 25 | ## Other notes 26 | If you find yourself unable to see the window (for example if you disconnect a monitor, or change your monitor resolutions), delete `listenMoeSettings.ini` and restart the application. This will reset the remembered location. 27 | 28 | # Todo 29 | - Code cleanup (let's face it, this will never happen) 30 | - Network disconnect detection (and reconnection) -- right now you can just pause and hit play again to reconnect if your network disconnects. 31 | -------------------------------------------------------------------------------- /Resources/DiscordRPC.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/DiscordRPC.dll -------------------------------------------------------------------------------- /Resources/Meiryo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/Meiryo.ttf -------------------------------------------------------------------------------- /Resources/close_inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/close_inverted.png -------------------------------------------------------------------------------- /Resources/cog_inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/cog_inverted.png -------------------------------------------------------------------------------- /Resources/fav_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/fav_sprite.png -------------------------------------------------------------------------------- /Resources/gripper-inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/gripper-inverted.png -------------------------------------------------------------------------------- /Resources/gripper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/gripper.png -------------------------------------------------------------------------------- /Resources/heart_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/heart_sprite.png -------------------------------------------------------------------------------- /Resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/icon.ico -------------------------------------------------------------------------------- /Resources/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/pause.png -------------------------------------------------------------------------------- /Resources/pause_ico.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/pause_ico.ico -------------------------------------------------------------------------------- /Resources/pause_inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/pause_inverted.png -------------------------------------------------------------------------------- /Resources/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/play.png -------------------------------------------------------------------------------- /Resources/play_ico.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/play_ico.ico -------------------------------------------------------------------------------- /Resources/play_inverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LISTEN-moe/windows-app/6044ea69993a59ddbb918c85da22403bf5a0c240/Resources/play_inverted.png -------------------------------------------------------------------------------- /Settings.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Globalization; 5 | using System.IO; 6 | using System.Reflection; 7 | using System.Text; 8 | 9 | namespace ListenMoeClient 10 | { 11 | enum Setting 12 | { 13 | //UI and form settings 14 | LocationX, 15 | LocationY, 16 | TopMost, 17 | SizeX, 18 | SizeY, 19 | FormOpacity, 20 | CustomColors, 21 | JPOPBaseColor, 22 | JPOPAccentColor, 23 | KPOPBaseColor, 24 | KPOPAccentColor, 25 | CustomBaseColor, 26 | CustomAccentColor, 27 | Scale, 28 | CloseToTray, 29 | HideFromAltTab, 30 | ThumbnailButton, 31 | 32 | //Visualiser settings 33 | EnableVisualiser, 34 | VisualiserResolutionFactor, 35 | FftSize, 36 | VisualiserBarWidth, 37 | VisualiserOpacity, 38 | VisualiserBars, 39 | VisualiserFadeEdges, 40 | JPOPVisualiserColor, 41 | KPOPVisualiserColor, 42 | CustomVisualiserColor, 43 | 44 | //Stream 45 | StreamType, 46 | 47 | //Misc 48 | UpdateAutocheck, 49 | UpdateInterval, 50 | Volume, 51 | OutputDeviceGuid, 52 | Token, 53 | Username, 54 | DiscordPresence 55 | } 56 | 57 | enum StreamType 58 | { 59 | Jpop, 60 | Kpop 61 | } 62 | 63 | //I should have just used a json serialiser 64 | static class Settings 65 | { 66 | public const int DEFAULT_WIDTH = 512; 67 | public const int DEFAULT_HEIGHT = 64; 68 | public const int DEFAULT_RIGHT_PANEL_WIDTH = 64; 69 | public const int DEFAULT_PLAY_PAUSE_SIZE = 20; 70 | 71 | private const string settingsFileLocation = "listenMoeSettings.ini"; 72 | 73 | static readonly object settingsMutex = new object(); 74 | static readonly object fileMutex = new object(); 75 | 76 | static Dictionary typedSettings = new Dictionary(); 77 | static readonly Dictionary typePrefixes = new Dictionary() 78 | { 79 | { 'i', typeof(int) }, 80 | { 'f', typeof(float) }, 81 | { 'b', typeof(bool) }, 82 | { 's', typeof(string) }, 83 | { 'c', typeof(Color) }, 84 | { 't', typeof(StreamType) } 85 | }; 86 | static readonly Dictionary reverseTypePrefixes = new Dictionary() 87 | { 88 | { typeof(int), 'i'}, 89 | { typeof(float), 'f'}, 90 | { typeof(bool), 'b'}, 91 | { typeof(string), 's'}, 92 | { typeof(Color), 'c' }, 93 | { typeof(StreamType), 't' } 94 | }; 95 | 96 | //Deserialisation 97 | static readonly Dictionary> parseActions = new Dictionary>() 98 | { 99 | { typeof(int), s => { 100 | bool success = int.TryParse(s, out int i); 101 | return (success, i); 102 | }}, 103 | { typeof(float), s => { 104 | bool success = float.TryParse(s, out float f); 105 | return (success, f); 106 | }}, 107 | { typeof(bool), s => { 108 | bool success = bool.TryParse(s, out bool b); 109 | return (success, b); 110 | }}, 111 | { typeof(string), s => { 112 | return (true, s); 113 | }}, 114 | { typeof(Color), s => { 115 | if (int.TryParse(s.Replace("#", ""), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int argb)) 116 | return (true, Color.FromArgb(255, Color.FromArgb(argb))); 117 | else 118 | throw new Exception("Could not parse color '" + s + "'. Check your settings file for any errors."); 119 | }}, 120 | { typeof(StreamType), s => { 121 | if (s == "jpop") 122 | return (true, StreamType.Jpop); 123 | else if (s == "kpop") 124 | return (true, StreamType.Kpop); 125 | throw new Exception("Could not parse StreamType."); 126 | }} 127 | }; 128 | 129 | //Serialisation 130 | static readonly Dictionary> saveActions = new Dictionary>() 131 | { 132 | { typeof(int), i => i.ToString() }, 133 | { typeof(float), f => f.ToString() }, 134 | { typeof(bool), b => b.ToString() }, 135 | { typeof(string), s => s }, 136 | { typeof(Color), c => ("#" + c.R.ToString("X2") + c.G.ToString("X2") + c.B.ToString("X2")).ToLowerInvariant() }, 137 | { typeof(StreamType), st => st == StreamType.Jpop ? "jpop" : "kpop" } 138 | }; 139 | 140 | static Settings() => LoadDefaultSettings(); 141 | 142 | public static T Get(Setting key) 143 | { 144 | lock (settingsMutex) 145 | { 146 | return ((Dictionary)(typedSettings[typeof(T)]))[key]; 147 | } 148 | } 149 | 150 | public static void Set(Setting key, T value) 151 | { 152 | Type t = typeof(T); 153 | lock (settingsMutex) 154 | { 155 | if (!typedSettings.ContainsKey(t)) 156 | { 157 | typedSettings.Add(t, new Dictionary()); 158 | } 159 | ((Dictionary)typedSettings[t])[key] = value; 160 | } 161 | } 162 | 163 | private static void LoadDefaultSettings() 164 | { 165 | Set(Setting.LocationX, 100); 166 | Set(Setting.LocationY, 100); 167 | Set(Setting.VisualiserResolutionFactor, 3); 168 | Set(Setting.UpdateInterval, 3600); //in seconds 169 | Set(Setting.SizeX, DEFAULT_WIDTH); 170 | Set(Setting.SizeY, DEFAULT_HEIGHT); 171 | Set(Setting.FftSize, 2048); 172 | 173 | Set(Setting.Volume, 0.3f); 174 | Set(Setting.VisualiserBarWidth, 3.0f); 175 | Set(Setting.VisualiserOpacity, 0.5f); 176 | Set(Setting.FormOpacity, 1.0f); 177 | Set(Setting.Scale, 1.0f); 178 | 179 | Set(Setting.TopMost, false); 180 | Set(Setting.UpdateAutocheck, true); 181 | Set(Setting.CloseToTray, false); 182 | Set(Setting.HideFromAltTab, false); 183 | Set(Setting.ThumbnailButton, true); 184 | Set(Setting.EnableVisualiser, true); 185 | Set(Setting.VisualiserBars, true); 186 | Set(Setting.VisualiserFadeEdges, false); 187 | 188 | Set(Setting.Token, ""); 189 | Set(Setting.Username, ""); 190 | Set(Setting.OutputDeviceGuid, ""); 191 | 192 | Set(Setting.JPOPVisualiserColor, Color.FromArgb(255, 1, 91)); 193 | Set(Setting.JPOPBaseColor, Color.FromArgb(33, 35, 48)); 194 | Set(Setting.JPOPAccentColor, Color.FromArgb(255, 1, 91)); 195 | 196 | Set(Setting.KPOPVisualiserColor, Color.FromArgb(48, 169, 237)); 197 | Set(Setting.KPOPBaseColor, Color.FromArgb(33, 35, 48)); 198 | Set(Setting.KPOPAccentColor, Color.FromArgb(48, 169, 237)); 199 | 200 | Set(Setting.CustomColors, false); 201 | Set(Setting.CustomVisualiserColor, Color.FromArgb(255, 1, 91)); 202 | Set(Setting.CustomBaseColor, Color.FromArgb(33, 35, 48)); 203 | Set(Setting.CustomAccentColor, Color.FromArgb(255, 1, 91)); 204 | 205 | Set(Setting.StreamType, StreamType.Jpop); 206 | Set(Setting.DiscordPresence, true); 207 | } 208 | 209 | public static void LoadSettings() 210 | { 211 | if (!File.Exists(settingsFileLocation)) 212 | { 213 | WriteSettings(); 214 | return; 215 | } 216 | 217 | string[] lines = File.ReadAllLines(settingsFileLocation); 218 | foreach (string line in lines) 219 | { 220 | string[] parts = line.Split(new char[] { '=' }, 2); 221 | if (string.IsNullOrWhiteSpace(parts[0])) 222 | continue; 223 | 224 | char prefix = parts[0][0]; 225 | Type t = typePrefixes[prefix]; 226 | Func parseAction = parseActions[t]; 227 | (bool success, object o) = parseAction(parts[1]); 228 | if (!success) 229 | continue; 230 | 231 | if (!Enum.TryParse(parts[0].Substring(1), out Setting settingKey)) 232 | continue; 233 | 234 | MethodInfo setMethod = typeof(Settings).GetMethod("Set", BindingFlags.Static | BindingFlags.Public); 235 | MethodInfo genericSet = setMethod.MakeGenericMethod(t); 236 | genericSet.Invoke(null, new object[] { settingKey, o }); 237 | } 238 | } 239 | 240 | public static void WriteSettings() 241 | { 242 | StringBuilder sb = new StringBuilder(); 243 | lock (settingsMutex) 244 | { 245 | foreach (KeyValuePair dict in typedSettings) 246 | { 247 | Type t = dict.Key; 248 | System.Collections.IDictionary typedDict = (System.Collections.IDictionary)dict.Value; 249 | Func saveAction = saveActions[t]; 250 | 251 | foreach (dynamic setting in typedDict) 252 | { 253 | sb.AppendLine(reverseTypePrefixes[t] + ((Setting)setting.Key).ToString() + "=" + saveAction(setting.Value)); 254 | } 255 | } 256 | } 257 | 258 | lock (fileMutex) 259 | { 260 | using (FileStream fileStream = new FileStream(settingsFileLocation, FileMode.Create, FileAccess.Write)) 261 | { 262 | using (StreamWriter streamWriter = new StreamWriter(fileStream)) 263 | streamWriter.Write(sb.ToString()); 264 | } 265 | } 266 | } 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /SpriteLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using System.Drawing.Imaging; 4 | using System.Runtime.InteropServices; 5 | 6 | namespace ListenMoeClient 7 | { 8 | class Sprite 9 | { 10 | public Image[] Frames; 11 | } 12 | 13 | class SpriteLoader 14 | { 15 | public static Sprite LoadFavSprite(Bitmap sheet, int frameSize) 16 | { 17 | Sprite result = new Sprite 18 | { 19 | Frames = new Image[sheet.Width / frameSize] 20 | }; 21 | //Split into frameSizexframeSize 22 | for (int i = 0; i < sheet.Width / frameSize; i++) 23 | { 24 | Bitmap bitmap = new Bitmap(frameSize, frameSize, PixelFormat.Format32bppArgb); 25 | using (Graphics g = Graphics.FromImage(bitmap)) 26 | g.DrawImage(sheet, new Rectangle(0, 0, frameSize, frameSize), new Rectangle(frameSize * i, 0, frameSize, frameSize), GraphicsUnit.Pixel); 27 | 28 | result.Frames[i] = bitmap; 29 | } 30 | 31 | return result; 32 | } 33 | 34 | public static void SetAlpha(Bitmap b, byte alpha) 35 | { 36 | BitmapData bmpData = b.LockBits(new Rectangle(0, 0, b.Width, b.Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); 37 | IntPtr p = bmpData.Scan0; 38 | byte[] pixels = new byte[b.Width * b.Height * 4]; //4 channels 39 | Marshal.Copy(p, pixels, 0, pixels.Length); 40 | 41 | for (int i = 0; i < pixels.Length; i += 4) 42 | { 43 | if (pixels[i + 3] == 0) 44 | continue; 45 | pixels[i + 3] = alpha; 46 | } 47 | 48 | Marshal.Copy(pixels, 0, p, pixels.Length); 49 | b.UnlockBits(bmpData); 50 | } 51 | 52 | public static Sprite LoadFadedFavSprite(Bitmap sheet, int frameSize) 53 | { 54 | Sprite result = new Sprite 55 | { 56 | Frames = new Image[2] 57 | }; 58 | 59 | int n = sheet.Width / frameSize; 60 | 61 | Bitmap b0 = new Bitmap(frameSize, frameSize, PixelFormat.Format32bppArgb); 62 | using (Graphics g = Graphics.FromImage(b0)) 63 | g.DrawImage(sheet, new Rectangle(0, 0, frameSize, frameSize), new Rectangle(0, 0, frameSize, frameSize), GraphicsUnit.Pixel); 64 | 65 | Bitmap b1 = new Bitmap(frameSize, frameSize, PixelFormat.Format32bppArgb); 66 | using (Graphics g = Graphics.FromImage(b1)) 67 | g.DrawImage(sheet, new Rectangle(0, 0, frameSize, frameSize), new Rectangle(frameSize * (n - 1), 0, frameSize, frameSize), GraphicsUnit.Pixel); 68 | 69 | SetAlpha(b0, 128); 70 | SetAlpha(b1, 128); 71 | 72 | result.Frames[0] = b0; 73 | result.Frames[1] = b1; 74 | 75 | return result; 76 | } 77 | 78 | private static Image DarkenBitmap(Image b) 79 | { 80 | Bitmap darkened = new Bitmap(b); 81 | 82 | ColorMatrix mat = new ColorMatrix 83 | { 84 | Matrix00 = 0.8f, 85 | Matrix11 = 0.8f, 86 | Matrix22 = 0.8f 87 | }; 88 | 89 | ImageAttributes attr = new ImageAttributes(); 90 | attr.SetColorMatrix(mat); 91 | 92 | using (Graphics g = Graphics.FromImage(darkened)) 93 | { 94 | g.DrawImage(b, new Rectangle(Point.Empty, b.Size), 0, 0, b.Width, b.Height, GraphicsUnit.Pixel, attr); 95 | } 96 | 97 | return darkened; 98 | } 99 | 100 | public static Sprite LoadDarkFavSprite(Bitmap sheet, int frameSize) 101 | { 102 | Sprite result = new Sprite 103 | { 104 | Frames = new Image[sheet.Width / frameSize] 105 | }; 106 | //Split into frameSizexframeSize 107 | for (int i = 0; i < sheet.Width / frameSize; i++) 108 | { 109 | Bitmap bitmap = new Bitmap(frameSize, frameSize, PixelFormat.Format32bppArgb); 110 | using (Graphics g = Graphics.FromImage(bitmap)) 111 | g.DrawImage(sheet, new Rectangle(0, 0, frameSize, frameSize), new Rectangle(frameSize * i, 0, frameSize, frameSize), GraphicsUnit.Pixel); 112 | 113 | result.Frames[i] = bitmap; 114 | } 115 | 116 | result.Frames[0] = DarkenBitmap(result.Frames[0]); 117 | 118 | return result; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Web/ReadFullyStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace ListenMoeClient 5 | { 6 | //Taken from the NAudio sample code 7 | public class ReadFullyStream : Stream 8 | { 9 | private readonly Stream sourceStream; 10 | private long pos; 11 | private readonly byte[] readAheadBuffer; 12 | private int readAheadLength; 13 | private int readAheadOffset; 14 | 15 | public ReadFullyStream(Stream sourceStream) 16 | { 17 | this.sourceStream = sourceStream; 18 | readAheadBuffer = new byte[4096]; 19 | } 20 | public override bool CanRead => true; 21 | 22 | public override bool CanSeek => false; 23 | 24 | public override bool CanWrite => false; 25 | 26 | public override void Flush() => throw new InvalidOperationException(); 27 | 28 | public override long Length => pos; 29 | 30 | public override long Position 31 | { 32 | get => pos; 33 | set => throw new InvalidOperationException(); 34 | } 35 | 36 | public override int Read(byte[] buffer, int offset, int count) 37 | { 38 | int bytesRead = 0; 39 | while (bytesRead < count) 40 | { 41 | int readAheadAvailableBytes = readAheadLength - readAheadOffset; 42 | int bytesRequired = count - bytesRead; 43 | if (readAheadAvailableBytes > 0) 44 | { 45 | int toCopy = Math.Min(readAheadAvailableBytes, bytesRequired); 46 | Array.Copy(readAheadBuffer, readAheadOffset, buffer, offset + bytesRead, toCopy); 47 | bytesRead += toCopy; 48 | readAheadOffset += toCopy; 49 | } 50 | else 51 | { 52 | readAheadOffset = 0; 53 | try 54 | { 55 | readAheadLength = sourceStream.Read(readAheadBuffer, 0, readAheadBuffer.Length); 56 | } 57 | catch (Exception) 58 | { 59 | //Read will throw an exception when pausing due to the thread dying, so we just ignore it. 60 | } 61 | //Debug.WriteLine(String.Format("Read {0} bytes (requested {1})", readAheadLength, readAheadBuffer.Length)); 62 | if (readAheadLength == 0) 63 | { 64 | break; 65 | } 66 | } 67 | } 68 | pos += bytesRead; 69 | return bytesRead; 70 | } 71 | 72 | public override long Seek(long offset, SeekOrigin origin) => throw new InvalidOperationException(); 73 | 74 | public override void SetLength(long value) => throw new InvalidOperationException(); 75 | 76 | public override void Write(byte[] buffer, int offset, int count) => throw new InvalidOperationException(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Web/SongInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using WebSocketSharp; 7 | using Newtonsoft.Json; 8 | 9 | namespace ListenMoeClient 10 | { 11 | 12 | public class WelcomeResponse 13 | { 14 | public int op { get; set; } 15 | public WelcomeResponseData d { get; set; } 16 | } 17 | 18 | public class WelcomeResponseData 19 | { 20 | public string message { get; set; } 21 | public WelcomeResponseUserData user { get; set; } 22 | public int heartbeat { get; set; } 23 | } 24 | 25 | public class WelcomeResponseUserData 26 | { 27 | public string uuid { get; set; } 28 | public string email { get; set; } 29 | public string username { get; set; } 30 | public string displayName { get; set; } 31 | public int uploads { get; set; } 32 | } 33 | 34 | public class SongInfoResponse 35 | { 36 | public int op { get; set; } 37 | public SongInfoResponseData d { get; set; } 38 | public string t { get; set; } 39 | } 40 | 41 | public class SongInfoResponseData 42 | { 43 | public Song song { get; set; } 44 | public Requester requester { get; set; } 45 | [JsonProperty("event")] 46 | public Event _event { get; set; } 47 | public DateTime startTime { get; set; } 48 | public Song[] lastPlayed { get; set; } 49 | public int listeners { get; set; } 50 | } 51 | 52 | public class Event 53 | { 54 | public int id { get; set; } 55 | public string name { get; set; } 56 | public string slug { get; set; } 57 | public string presence { get; set; } 58 | public string image { get; set; } 59 | } 60 | 61 | public class Requester 62 | { 63 | public string uuid { get; set; } 64 | public string username { get; set; } 65 | public string displayName { get; set; } 66 | } 67 | 68 | public class Song 69 | { 70 | public int id { get; set; } 71 | public string title { get; set; } 72 | public Source[] sources { get; set; } 73 | public Artist[] artists { get; set; } 74 | public Album[] albums { get; set; } 75 | public int duration { get; set; } 76 | public bool favorite { get; set; } 77 | } 78 | 79 | public class Source 80 | { 81 | public int id { get; set; } 82 | public string name { get; set; } 83 | public string nameRomaji { get; set; } 84 | public object image { get; set; } 85 | } 86 | 87 | public class Artist 88 | { 89 | public int id { get; set; } 90 | public string name { get; set; } 91 | public string nameRomaji { get; set; } 92 | public object image { get; set; } 93 | } 94 | 95 | public class Album 96 | { 97 | public int id { get; set; } 98 | public string name { get; set; } 99 | public string nameRomaji { get; set; } 100 | public string image { get; set; } 101 | } 102 | 103 | public class SongInfoStream 104 | { 105 | private WebSocket socket; 106 | private TaskFactory factory; 107 | public delegate void StatsReceived(SongInfoResponseData info); 108 | public event StatsReceived OnSongInfoReceived; 109 | public SongInfoResponseData currentInfo; 110 | 111 | private const string JPOP_SOCKET_ADDR = "wss://listen.moe/gateway"; 112 | private const string KPOP_SOCKET_ADDR = "wss://listen.moe/kpop/gateway"; 113 | 114 | private Thread heartbeatThread; 115 | private CancellationTokenSource cts; 116 | 117 | public SongInfoStream(TaskFactory factory) 118 | { 119 | this.factory = factory; 120 | Reconnect(); 121 | } 122 | 123 | public void Reconnect() 124 | { 125 | socket?.Close(); 126 | string address = Settings.Get(Setting.StreamType) == StreamType.Jpop ? JPOP_SOCKET_ADDR : KPOP_SOCKET_ADDR; 127 | socket = new WebSocket(address); 128 | 129 | socket.OnMessage += (sender, e) => ParseSongInfo(e.Data); 130 | socket.OnError += (sender, e) => throw e.Exception; 131 | socket.OnClose += (sender, e) => Connect(); 132 | 133 | socket.SslConfiguration.EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12; 134 | Connect(); 135 | } 136 | 137 | private void Connect() 138 | { 139 | try 140 | { 141 | socket.Connect(); 142 | Authenticate(); 143 | } 144 | catch (Exception) {} 145 | } 146 | 147 | public void Authenticate() 148 | { 149 | try 150 | { 151 | string token; 152 | if (User.LoggedIn) 153 | token = "Bearer " + Settings.Get(Setting.Token); 154 | else 155 | token = ""; 156 | 157 | socket.Send("{ \"op\": 0, \"d\": { \"auth\": \"" + token + "\" } }"); 158 | } 159 | catch (Exception) {} 160 | } 161 | 162 | private void SendHeartbeat() 163 | { 164 | try 165 | { 166 | socket.Send("{ \"op\": 9 }"); 167 | } catch (Exception) { } 168 | } 169 | 170 | private void ProcessWelcomeResponse(WelcomeResponse resp) 171 | { 172 | int heartbeatInterval = resp.d.heartbeat; 173 | 174 | if (heartbeatThread != null) 175 | { 176 | cts.Cancel(); 177 | 178 | //Running on the websocket thread, so no need to async/await 179 | heartbeatThread.Join(); 180 | } 181 | 182 | cts = new CancellationTokenSource(); 183 | heartbeatThread = new Thread(() => 184 | { 185 | Stopwatch watch = new Stopwatch(); 186 | watch.Start(); 187 | long lastMillis = watch.ElapsedMilliseconds; 188 | while (!cts.IsCancellationRequested) 189 | { 190 | long currentMillis = watch.ElapsedMilliseconds; 191 | if (currentMillis - lastMillis > heartbeatInterval) { 192 | SendHeartbeat(); 193 | lastMillis = currentMillis; 194 | } 195 | Thread.Sleep(1000); 196 | } 197 | }); 198 | heartbeatThread.Start(); 199 | } 200 | 201 | private string Clean(string input) => input != null ? input.Trim().Replace('\n', ' ') : ""; 202 | 203 | private void ParseSongInfo(string data) 204 | { 205 | string noWhitespaceData = new string(data.Where(c => !char.IsWhiteSpace(c)).ToArray()); 206 | if (noWhitespaceData.Contains("\"op\":0")) 207 | { 208 | //Identify/Welcome response 209 | WelcomeResponse resp = JsonConvert.DeserializeObject(data); 210 | ProcessWelcomeResponse(resp); 211 | } 212 | else if (noWhitespaceData.Contains("\"op\":1")) 213 | { 214 | //Song info 215 | SongInfoResponse resp = JsonConvert.DeserializeObject(data); 216 | if (resp.t != "TRACK_UPDATE") 217 | return; 218 | 219 | currentInfo = resp.d; 220 | currentInfo.song.sources = currentInfo.song.sources ?? new Source[0]; 221 | foreach (Source source in currentInfo.song.sources) 222 | { 223 | source.name = Clean(source.name); 224 | } 225 | 226 | foreach (Artist artist in currentInfo.song.artists) 227 | { 228 | artist.name = Clean(artist.name); 229 | } 230 | currentInfo.song.title = Clean(currentInfo.song.title); 231 | 232 | if (currentInfo.requester != null) 233 | { 234 | currentInfo.requester.displayName = Clean(currentInfo.requester.displayName); 235 | currentInfo.requester.username = Clean(currentInfo.requester.username); 236 | } 237 | 238 | if (currentInfo._event != null) 239 | currentInfo._event.name = Clean(currentInfo._event.name); 240 | 241 | factory.StartNew(() => 242 | { 243 | OnSongInfoReceived(currentInfo); 244 | }); 245 | } 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /Web/Updater.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Net; 6 | using System.Runtime.Serialization; 7 | using System.Threading.Tasks; 8 | using System.Windows.Forms; 9 | 10 | namespace ListenMoeClient 11 | { 12 | class Updater 13 | { 14 | class LatestReleaseResponse 15 | { 16 | public string tag_name { get; set; } 17 | public LatestReleaseAsset[] assets { get; set; } 18 | } 19 | 20 | class LatestReleaseAsset 21 | { 22 | public string browser_download_url { get; set; } 23 | public string name { get; set; } 24 | } 25 | 26 | private const string UPDATE_ENDPOINT = "https://api.github.com/repos/LISTEN-moe/windows-app/releases/latest"; 27 | 28 | public static async Task CheckGithubVersion() 29 | { 30 | (bool success, string rawResponse) = await WebHelper.Get(UPDATE_ENDPOINT, false); 31 | LatestReleaseResponse response = JsonConvert.DeserializeObject(rawResponse); 32 | 33 | var version = response.tag_name; 34 | if (version == null) 35 | return false; 36 | 37 | if (version.StartsWith("v")) 38 | version = version.Substring(1); 39 | 40 | Console.WriteLine(Globals.VERSION); 41 | Console.WriteLine(version); 42 | 43 | //Same version 44 | if (version.Trim() == Globals.VERSION) 45 | return false; 46 | 47 | var latestParts = version.Trim().Split(new char[] { '.' }); 48 | var ourParts = Globals.VERSION.Split(new char[] { '.' }); 49 | 50 | //Must be really out of date if we've changed versioning schemes... 51 | if (latestParts.Length != ourParts.Length) 52 | return true; 53 | 54 | //Compare sub version numbers 55 | for (int i = 0; i < latestParts.Length; i++) 56 | { 57 | if (!int.TryParse(latestParts[i], out int latestVers)) 58 | return true; 59 | if (!int.TryParse(ourParts[i], out int ourVers)) 60 | return true; 61 | 62 | if (latestVers == ourVers) 63 | continue; 64 | else 65 | return latestVers > ourVers; 66 | } 67 | 68 | return false; 69 | } 70 | 71 | public static async Task UpdateToNewVersion(DownloadProgressChangedEventHandler dpceh, System.ComponentModel.AsyncCompletedEventHandler aceh) 72 | { 73 | (bool success, string rawResponse) = await WebHelper.Get(UPDATE_ENDPOINT, false); 74 | LatestReleaseResponse response = JsonConvert.DeserializeObject(rawResponse); 75 | 76 | if (response.assets.Length == 0) 77 | { 78 | MessageBox.Show("Unable to download new executable. Please update manually from the Github releases page."); 79 | return; 80 | } 81 | 82 | //First download link is fine for now... probably 83 | var link = response.assets[0].browser_download_url; 84 | 85 | var downloadPath = Path.GetTempFileName(); 86 | WebClient wc = new WebClient(); 87 | wc.DownloadProgressChanged += dpceh; 88 | wc.DownloadFileCompleted += aceh; 89 | await wc.DownloadFileTaskAsync(link, downloadPath); 90 | 91 | //Rename current executable as backup 92 | try 93 | { 94 | //Wait for a second before restarting so we get to see our nice green finished bar 95 | await Task.Delay(1000); 96 | 97 | string exeName = Process.GetCurrentProcess().ProcessName; 98 | File.Delete(exeName + ".bak"); 99 | File.Move(exeName + ".exe", exeName + ".bak"); 100 | File.Move(downloadPath, exeName + ".exe"); 101 | 102 | Process.Start(exeName + ".exe"); 103 | Environment.Exit(0); 104 | } 105 | catch (Exception) 106 | { 107 | MessageBox.Show("Unable to replace with updated executable. Check whether the executable is marked as read-only, or whether it is in a protected folder that requires administrative rights."); 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Web/User.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace ListenMoeClient 7 | { 8 | class User 9 | { 10 | private static bool loggedIn = false; 11 | public static event Action OnLoginComplete; 12 | public static event Action OnLogout; 13 | 14 | public static bool LoggedIn => loggedIn; 15 | 16 | /// 17 | /// Attempts to login with the specified credentials. Returns an AuthenticateResponse, containing 18 | /// whether or not the request was successful, along with the token and failure message if unsuccessful. 19 | /// 20 | /// 21 | /// 22 | /// 23 | public static async Task Login(string username, string password) 24 | { 25 | Dictionary postData = new Dictionary 26 | { 27 | { "username", username }, 28 | { "password", password } 29 | }; 30 | 31 | (bool success, string resp) = await WebHelper.Post("https://listen.moe/api/login", postData, true); 32 | AuthenticateResponse response = JsonConvert.DeserializeObject(resp); 33 | if (success) 34 | { 35 | loggedIn = true; 36 | //Save successful credentials 37 | Settings.Set(Setting.Username, username); 38 | Settings.Set(Setting.Token, response.token); 39 | Settings.WriteSettings(); 40 | 41 | if (!response.mfa) 42 | { 43 | OnLoginComplete(); 44 | } 45 | } 46 | else 47 | { 48 | //Login failure; clear old saved credentials 49 | Settings.Set(Setting.Username, ""); 50 | } 51 | 52 | return response; 53 | } 54 | 55 | /// 56 | /// Attempts to login with the specified auth token. Returns whether or not the login was successful. 57 | /// 58 | /// 59 | /// 60 | public static async Task Login(string token) 61 | { 62 | (bool success, string resp) = await WebHelper.Get("https://listen.moe/api/users/@me", token, true); 63 | ListenMoeResponse response = JsonConvert.DeserializeObject(resp); 64 | if (success) 65 | { 66 | loggedIn = true; 67 | OnLoginComplete(); 68 | } 69 | else 70 | { 71 | //Clear saved credentials on failure 72 | Settings.Set(Setting.Username, ""); 73 | Settings.Set(Setting.Token, ""); 74 | Settings.WriteSettings(); 75 | } 76 | return loggedIn; 77 | } 78 | 79 | public static async Task LoginMfa(string mfaCode) 80 | { 81 | Dictionary postData = new Dictionary 82 | { 83 | { "token", mfaCode } 84 | }; 85 | 86 | string token = Settings.Get(Setting.Token); 87 | (bool success, string resp) = await WebHelper.Post("https://listen.moe/api/login/mfa", token, postData, true); 88 | AuthenticateResponse response = JsonConvert.DeserializeObject(resp); 89 | if (success) 90 | { 91 | loggedIn = true; 92 | Settings.Set(Setting.Token, response.token); 93 | Settings.WriteSettings(); 94 | 95 | OnLoginComplete(); 96 | } 97 | return success; 98 | } 99 | 100 | public static async Task FavoriteSong(string id, bool favorite) 101 | { 102 | string token = Settings.Get(Setting.Token); 103 | bool success = false; 104 | string result = null; 105 | 106 | if (favorite) 107 | (success, result) = await WebHelper.Post("https://listen.moe/api/favorites/" + id, token, null, true); 108 | else 109 | (success, result) = await WebHelper.Delete("https://listen.moe/api/favorites/" + id, token, true); 110 | 111 | return success; 112 | } 113 | 114 | public static void Logout() 115 | { 116 | loggedIn = false; 117 | Settings.Set(Setting.Username, ""); 118 | Settings.Set(Setting.Token, ""); 119 | Settings.WriteSettings(); 120 | OnLogout(); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Web/WebHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Net; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using System.Web; 7 | 8 | namespace ListenMoeClient 9 | { 10 | public class ListenMoeResponse 11 | { 12 | public string message { get; set; } 13 | } 14 | 15 | public class AuthenticateResponse : ListenMoeResponse 16 | { 17 | public string token { get; set; } 18 | public bool mfa { get; set; } 19 | } 20 | 21 | class WebHelper 22 | { 23 | private static byte[] CreatePostData(Dictionary postData) 24 | { 25 | if (postData == null) 26 | return Encoding.UTF8.GetBytes("{ }"); 27 | 28 | StringBuilder result = new StringBuilder("{"); 29 | foreach (KeyValuePair keyValuePair in postData) 30 | { 31 | result.Append("\"" + HttpUtility.JavaScriptStringEncode(keyValuePair.Key) + "\":"); 32 | result.Append("\"" + HttpUtility.JavaScriptStringEncode(keyValuePair.Value) + "\","); 33 | } 34 | result[result.Length - 1] = '}'; 35 | 36 | return Encoding.UTF8.GetBytes(result.ToString()); 37 | } 38 | 39 | private static HttpWebRequest CreateWebRequest(string url, string token, bool isListenMoe, string method) 40 | { 41 | HttpWebRequest hwr = WebRequest.CreateHttp(url); 42 | hwr.Method = method; 43 | hwr.Timeout = 2000; 44 | hwr.UserAgent = Globals.USER_AGENT; 45 | if (token.Trim() != "") 46 | hwr.Headers["authorization"] = "Bearer " + token; 47 | 48 | if (isListenMoe) 49 | hwr.Accept = "application/vnd.listen.v4+json"; 50 | 51 | return hwr; 52 | } 53 | 54 | private static async Task<(bool, string)> GetResponse(HttpWebRequest hwr) 55 | { 56 | Stream respStream; 57 | bool success = true; 58 | try 59 | { 60 | respStream = (await hwr.GetResponseAsync()).GetResponseStream(); 61 | } 62 | catch (WebException e) 63 | { 64 | success = false; 65 | respStream = e.Response.GetResponseStream(); 66 | } 67 | 68 | string result = await new StreamReader(respStream).ReadToEndAsync(); 69 | return (success, result); 70 | } 71 | 72 | public static async Task<(bool, string)> Post(string url, string token, Dictionary postData, bool isListenMoe) => await Post(url, token, postData, "application/json", isListenMoe); 73 | 74 | public static async Task<(bool, string)> Post(string url, Dictionary postData, bool isListenMoe) => await Post(url, "", postData, "application/json", isListenMoe); 75 | 76 | public static async Task<(bool, string)> Post(string url, string token, Dictionary postData, string contentType, bool isListenMoe) 77 | { 78 | HttpWebRequest hwr = CreateWebRequest(url, token, isListenMoe, "POST"); 79 | 80 | byte[] postDataBytes = CreatePostData(postData); 81 | hwr.ContentType = contentType; 82 | hwr.ContentLength = postDataBytes.Length; 83 | 84 | Stream reqStream = await hwr.GetRequestStreamAsync(); 85 | reqStream.Write(postDataBytes, 0, postDataBytes.Length); 86 | reqStream.Flush(); 87 | reqStream.Close(); 88 | 89 | return await GetResponse(hwr); 90 | } 91 | 92 | public static async Task<(bool, string)> Get(string endpoint, bool isListenMoe) => await Get(endpoint, "", isListenMoe); 93 | 94 | public static async Task<(bool, string)> Get(string url, string token, bool isListenMoe) 95 | { 96 | HttpWebRequest hwr = CreateWebRequest(url, token, isListenMoe, "GET"); 97 | return await GetResponse(hwr); 98 | } 99 | 100 | public static async Task<(bool, string)> Delete(string url, string token, bool isListenMoe) 101 | { 102 | HttpWebRequest hwr = CreateWebRequest(url, token, isListenMoe, "DELETE"); 103 | return await GetResponse(hwr); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 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 | 46 | --------------------------------------------------------------------------------