├── .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 | 
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 |
--------------------------------------------------------------------------------