Native Architecture
Built as a pure C++ Windhawk mod. By injecting directly into the VLC process, we eliminate the need for background Node.js scripts or separate applications. Zero system tray clutter, zero overhead.
Automatically show your movies and music as your Discord status. It's fast, lightweight, and works perfectly in the background.


Unlike other RPC solutions that require separate background apps, our native Windhawk mod lives directly inside VLC. It only runs when VLC runs, consumes zero resources when you're not watching, and requires no manual startup. It just works.
Built as a pure C++ Windhawk mod. By injecting directly into the VLC process, we eliminate the need for background Node.js scripts or separate applications. Zero system tray clutter, zero overhead.
Automatically strips scene tags (WEB-DL, x265) and piracy URLs. Your status stays clean and professional.
100% local. Your playback data and file paths never leave your machine.
TV shows show Season/Episode. Music shows Artist/Album. Includes active audio language and quality badges.
Automatic local art uploads with Bing fallback for posters.
Configurable search button for Google, IMDb, or YouTube.
Preview exactly how your Discord status will look. Tweak the settings here to see how the mod adapts to your preferences.

Standard mode shows play/pause status in the corner of the media icon.
Metadata Cleaner: Automatically strips scene tags (WEB-DL, x265) and piracy URLs from filenames.
Local Filters: Keep title cleaning offline to ensure 100% privacy and zero external requests.
Chapter Support: Show the current chapter number for movies and anime.
Audio Language: Displays the active audio track language (e.g., EN, JP) in your status.
Windows Toasts: Get a native Windows notification whenever a new track or movie starts.
Custom Port: Easily adjust the VLC communication port if you have a non-standard setup.
Native, lightweight, and engineered to work without background overhead. Follow these simple steps to transform your Discord presence.
Get the lightweight mod manager from windhawk.net. It's the engine that powers our native VLC integration.
Open Windhawk, Go to Explore tab, and search for 'VLC Discord RPC'. One click to install.
Once installed, the mod is active. Configure VLC to start sharing your status.
To allow the mod to communicate with VLC, you need to enable the web interface. This is a one-time setup.
Enable Web Interface
Go to Tools > Preferences > All > Interface > Main interfaces. Check Web.
Set Password
Under Lua HTTP, set any password. The mod will auto-detect it, but use 1234 if it doesn't work.
Restart VLC
Save your settings and restart VLC for changes to take effect.
VLC is now ready to share your media status with Discord.
Every line of code is public. Read it, audit it, improve it.
// ==WindhawkMod==
// @id vlc-discord-rpc
// @name VLC Discord Rich Presence
// @description Shows your currently playing media on Discord — with cover art, quality tags, and a search button.
// @version 1.1.5
// @author ciizerr
// @github https://github.com/ciizerr
// @homepage https://vlc-rpc.vercel.app/
// @include vlc.exe
// @compilerOptions -lwinhttp -lshell32 -lgdiplus -lole32
// @architecture x86
// @architecture x86-64
// @license MIT
// ==/WindhawkMod==
// ==WindhawkModReadme==
/*
# VLC Discord Rich Presence
Shows what you're watching or listening to on your Discord profile — including cover art, quality tags, and a search button.
## Features
| Feature | Description |
|---|---|
| 🖼️ **Cover Art** | Uploads local album art automatically. Fallbacks to searching the web for a matching poster. |
| 🧹 **Smart Title Cleaning** | Strips scene tags, piracy site URLs, and release info from filenames so titles look clean. |
| 🎮 **Activity Type** | Automatically shows "Listening to", "Watching", or "Playing" based on the file type. |
| 🎵 **Rich Metadata** | Music shows artist/album. Video shows season, episode, chapter, and audio language. |
| 📺 **Quality Tags** | Displays resolution and format badges like 4K, HDR, or 1080p. |
| 🔍 **Search Button** | Adds an interactive button linking to Google, IMDb, YouTube, or a custom URL. |
| 🎨 **Themes** | Choose between a classic Default or sleek Dark icon set. |
| 📉 **Minimal Mode** | Hides the small play/pause badge so your cover art takes center stage. |
## Icon Themes
- **Default** — The classic orange VLC cone.

- **Dark** — A dark variant for low-light setups.

Community icon submissions are welcome — reach out to `ciizerr` on Discord with a set of four icons (vlc, play, pause, stop).
## First-Time Setup
This mod reads playback data from VLC's built-in HTTP interface. You only need to do this once.
1. Open VLC and go to **Tools → Preferences** (`Ctrl+P`).
2. In the bottom-left, switch *Show settings* to **All**.
3. Go to **Interface → Main interfaces** and check **Web**.

4. Expand *Main interfaces* in the sidebar and click **Lua**.
5. Under *Lua HTTP*, set the **Password** to `1234`. Leave the port to default.

6. Click **Save** and restart VLC.
## Settings Overview
| Setting | What it does |
|---|---|
| **Cover Art** | Fetches and shows artwork on your status |
| **Quality Tags** | Shows 4K / HDR / 1080p badges |
| **Minimal Mode** | Hides the small play/pause corner badge |
| **Clean Titles** | Removes scene tags and URLs from filenames |
| **Title Filters** | Choose between live community filters or local-only |
| **Custom Filter Words** | Add your own words to strip from titles |
| **Notifications** | Shows a Windows toast when a new track starts |
| **Search Button** | Picks which site the status button links to |
| **"Listening to..." label** | Controls what shows in the activity name for music |
## Filter List
To keep title cleaning accurate, the mod can download a small community‑maintained filter list from GitHub.
This list is refreshed every 6 hours.
If you enable **Local Filters Only** in settings, the mod will stop downloading the online filter list and instead use:
- The built‑in word list
- Any custom filters you’ve added yourself
## Troubleshooting
| Problem | Solution |
|---|---|
| **Before trying fixes** | Check the Windhawk mod logs first to identify the issue. |
| **Not showing on Discord** | Enable **Display current activity** in Discord. In VLC, enable **Web** interface and set Lua HTTP password to `1234`. Restart VLC afterward. |
| **Connection still fails (rare)** | Open `%APPDATA%\vlc\vlcrc`, change `#http-port=8080` to `http-port=8080`, save, and restart VLC. |
| **Could not reach VLC** | VLC may be using another port. Open **Resource Monitor** → **Network** → **Listening Ports**, find `vlc.exe`, and use that port in the mod settings. |
| **Logs show "401 Unauthorized"** | Wrong VLC password. Ensure Lua HTTP password is exactly `1234`. Restart VLC afterward. |
| **Wrong poster/artwork** | Enable **Clean Titles** for better filename cleanup and poster matching. |
## External Requests
The mod makes a few external requests for certain features:
- **`www.bing.com`** — used to search for poster and artwork URLs (`FindExternalArtwork`)
- **`uguu.se`** — used to upload local album art (`UploadToUguu`)
- **`raw.githubusercontent.com`** — used to download `vlc-discord-icon.ico` and `filters.txt` for notifications and media title cleaning
These requests are only used for artwork, uploads, notifications, and media title cleaning.
## Support & Feedback
Found a bug, have a suggestion, or need help?
- **Discord:** `ciizerr` ([Windhawk Server](https://discord.com/servers/windhawk-923944342991818753))
- **GitHub:** [vlc-discord-rpc-archive](https://github.com/ciizerr/vlc-discord-rpc-archive)
*/
// ==/WindhawkModReadme==
// ==WindhawkModSettings==
/*
- ShowCoverArt: true
$name: Cover Art
$description: "Fetches and displays artwork on your Discord status. Uses local album art if available, otherwise searches the web for a matching poster."
- ShowQualityTags: true
$name: Quality Tags
$description: "Shows resolution and format badges like 4K, HDR, or 1080p next to the title."
- MinimalMode: false
$name: Minimal Mode
$description: "Hides the small play/pause/stop badge in the corner of your status image."
- EnableMetadataCleaner: true
$name: Clean Media Titles
$description: "Strips scene release tags (e.g. WEB-DL, x265), piracy site URLs, and other clutter from filenames before showing them on Discord."
- StrictLocalMode: true
$name: Local Filters Only
$description: "Keeps title cleaning offline. When off, the mod downloads an updated community filter list from GitHub every 6 hours. When on, it only uses the built-in filters and your custom words below."
- CustomJunkWords: ""
$name: Custom Words to Remove
$description: "Extra words or site names to strip from titles, separated by commas. Example: toonworld4all.com, mytag. Requires 'Clean Media Titles' to be enabled."
- ShowNotifications: false
$name: Toast Notifications
$description: "Shows a Windows notification when a new file starts playing."
- ShowChapter: true
$name: Show Chapter Number
$description: "Displays the current chapter alongside the episode or track info."
- ShowAudioLanguage: true
$name: Show Audio Language
$description: "Shows the active audio track language (e.g. EN, JP) in your status."
- MusicActivityName: "Title"
$name: "'Listening to...' Label"
$description: "What to show in the Discord activity name when playing music."
$options:
- Title: Song Title
- Artist: Artist Name
- Album: Album Name
- Provider: Google
$name: Search Button
$description: "The site your Discord status button links to when clicked."
$options:
- Google: Google
- Bing: Bing
- IMDb: IMDb
- YouTube: YouTube
- Custom: Custom URL
- CustomUrl: ""
$name: Custom Search URL
$description: "Only used when Search Button is set to Custom. Paste the base search URL here. Example: https://myanimelist.net/search/all?q="
- ButtonLabel: "Search This"
$name: Button Label
$description: "The text shown on the Discord status button. Maximum 30 characters."
- Theme: ""
$name: Icon Theme
$description: "Visual style of the main icon on your Discord status."
$options:
- "": Default
- "dark_": Dark Mode
- CustomPort: 0
$name: Custom VLC Port
$description: "Only needed if VLC is running on a non-standard HTTP port. Leave at 0 to detect automatically."
- ClientId: "1465711556418474148"
$name: Discord Application ID
$description: "The Client ID from the Discord Developer Portal. Only change this if you've set up your own Discord application with custom assets."
*/
// ==/WindhawkModSettings==
#include <windows.h>
#include <shellapi.h>
#include <objbase.h>
#include <gdiplus.h>
#include <winhttp.h>
#include <string>
#include <thread>
#include <vector>
#include <cstdio>
#include <atomic>
#include <map>
#include <fstream>
#include <mutex>
#include <algorithm>
#include <cctype>
#include <sstream>
#include <shlobj.h>
#include <sys/stat.h>
#pragma comment(lib, "shell32.lib")
// =============================================================
// ⚙️ GLOBALS
// =============================================================
ULONG_PTR g_gdiplusToken = 0;
std::atomic<bool> g_stopThread{false};
std::thread g_workerThread;
const std::string SEP = " \xE2\x97\x8F ";
std::map<std::string, std::string> g_imageCache;
std::map<std::string, bool> g_fetchInProgress;
std::mutex g_cacheMutex;
// =============================================================
// 🔥 DYNAMIC JUNK FILTER GLOBALS 🔥
// =============================================================
std::vector<std::string> g_junkSites;
std::vector<std::string> g_tlds;
std::vector<std::string> g_truncateTags;
std::vector<std::string> g_junkWords;
std::mutex g_filterMutex;
bool g_filtersLoaded = false;
// =============================================================
// 1. STRING & METADATA HELPERS
// =============================================================
std::string Base64Encode(const std::string& in) {
std::string out;
int val = 0, valb = -6;
for (unsigned char c : in) {
val = (val << 8) + c;
valb += 8;
while (valb >= 0) {
out.push_back("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(val >> valb) & 0x3F]);
valb -= 6;
}
}
if (valb > -6) out.push_back("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[((val << 8) >> (valb + 8)) & 0x3F]);
while (out.size() % 4) out.push_back('=');
return out;
}
std::string WStrToStr(const std::wstring& wstr) {
if (wstr.empty()) return "";
int size = WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), NULL, 0, NULL, NULL);
std::string str(size, 0);
WideCharToMultiByte(CP_UTF8, 0, &wstr[0], (int)wstr.size(), &str[0], size, NULL, NULL);
return str;
}
std::wstring StrToWStr(const std::string& str) {
if (str.empty()) return L"";
int size = MultiByteToWideChar(CP_UTF8, 0, &str[0], (int)str.size(), NULL, 0);
std::wstring wstr(size, 0);
MultiByteToWideChar(CP_UTF8, 0, &str[0], (int)str.size(), &wstr[0], size);
return wstr;
}
void ReadVlcConfig(int& port, std::wstring& authBase64, std::string& rawPassword) {
port = 0;
std::string password = "";
char appDataPath[MAX_PATH];
if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appDataPath))) {
std::string vlcrcPath = std::string(appDataPath) + "\\vlc\\vlcrc";
std::ifstream file(vlcrcPath);
if (file.is_open()) {
std::string line;
while (std::getline(file, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
if (line.find("http-port=") == 0) {
try { port = std::stoi(line.substr(10)); } catch(...) {}
}
else if (line.find("http-password=") == 0) {
password = line.substr(14);
}
}
} else {
Wh_Log(L"[VLC-RPC] Could not open vlcrc at %S — using defaults.", vlcrcPath.c_str());
}
} else {
Wh_Log(L"[VLC-RPC] Could not resolve %%APPDATA%% path.");
}
if (password.empty()) {
password = "1234";
}
std::string authStr = ":" + password;
std::string b64 = Base64Encode(authStr);
authBase64 = StrToWStr(b64);
rawPassword = password;
}
std::string UrlEncode(const std::string &value) {
static const char hex[] = "0123456789ABCDEF";
std::string result;
for (char c : value) {
if (isalnum((unsigned char)c) || c == '-' || c == '_' || c == '.' || c == '~') {
result += c;
} else {
result += '%';
result += hex[(c >> 4) & 0xF];
result += hex[c & 0xF];
}
}
return result;
}
std::string UrlDecode(const std::string &value) {
std::string result;
result.reserve(value.length());
for (size_t i = 0; i < value.length(); ++i) {
if (value[i] == '%') {
if (i + 2 < value.length()) {
int hex1 = value[i + 1];
int hex2 = value[i + 2];
hex1 = (hex1 >= '0' && hex1 <= '9') ? (hex1 - '0') : ((hex1 & 0xDF) - 'A' + 10);
hex2 = (hex2 >= '0' && hex2 <= '9') ? (hex2 - '0') : ((hex2 & 0xDF) - 'A' + 10);
result += static_cast<char>((hex1 << 4) | hex2);
i += 2;
}
} else if (value[i] == '+') {
result += ' ';
} else {
result += value[i];
}
}
return result;
}
std::string SanitizeString(const std::string& s) {
std::string out;
for (char c : s) {
if (c == '"') out += '\'';
else if (c == '\\') {}
else if ((unsigned char)c < 32) {}
else out += c;
}
return out;
}
std::string NumToStr(long long num) { return std::to_string(num); }
std::string ExtractString(const std::string& json, const std::string& key) {
std::string search = "\"" + key + "\":\"";
size_t start = json.find(search);
if (start == std::string::npos) return "";
start += search.length();
size_t current = start;
while (current < json.length()) {
size_t nextQuote = json.find("\"", current);
if (nextQuote == std::string::npos) return "";
if (nextQuote > 0 && json[nextQuote - 1] == '\\') {
current = nextQuote + 1;
} else {
return json.substr(start, nextQuote - start);
}
}
return "";
}
long long ExtractNumber(const std::string& json, const std::string& key) {
std::string search = "\"" + key + "\":";
size_t start = json.find(search);
if (start == std::string::npos) return -1;
start += search.length();
if (json[start] == '"') {
start++;
size_t end = json.find("\"", start);
if (end == std::string::npos) return -1;
try { return (long long)std::stod(json.substr(start, end - start)); } catch(...) { return -1; }
}
size_t endComma = json.find(",", start);
size_t endBrace = json.find("}", start);
size_t end = (endComma < endBrace) ? endComma : endBrace;
if (end == std::string::npos) return -1;
try { return (long long)std::stod(json.substr(start, end - start)); } catch(...) { return -1; }
}
std::string CleanString(std::string str) {
std::string out;
for (size_t i = 0; i < str.length(); ++i) {
if (str[i] == '%' && i + 2 < str.length()) {
if (str.substr(i, 3) == "%20") { out += ' '; i += 2; continue; }
if (str.substr(i, 3) == "%5B") { out += '['; i += 2; continue; }
if (str.substr(i, 3) == "%5D") { out += ']'; i += 2; continue; }
}
out += str[i];
}
return out;
}
std::string CleanMetadata(std::string text, const std::vector<std::string>& customJunk) {
if (text.empty()) return "";
std::string noBrackets = "";
bool inBracket = false;
for (char c : text) {
if (c == '[') inBracket = true;
else if (c == ']') { inBracket = false; continue; }
if (!inBracket) noBrackets += c;
}
size_t dot = noBrackets.find_last_of(".");
if (dot != std::string::npos) {
std::string ext = noBrackets.substr(dot);
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
if (ext == ".mp3" || ext == ".mkv" || ext == ".mp4" || ext == ".avi" || ext == ".flac" || ext == ".m4a" || ext == ".wav") {
noBrackets = noBrackets.substr(0, dot);
}
}
std::string lowerText = noBrackets;
std::transform(lowerText.begin(), lowerText.end(), lowerText.begin(), ::tolower);
for (const auto& word : customJunk) {
if (word.empty()) continue;
size_t pos;
while ((pos = lowerText.find(word)) != std::string::npos) {
noBrackets.replace(pos, word.length(), std::string(word.length(), ' '));
lowerText.replace(pos, word.length(), std::string(word.length(), ' '));
}
}
std::vector<std::string> activeSites;
std::vector<std::string> activeTlds;
std::vector<std::string> activeTags;
std::vector<std::string> activeWords;
{
std::lock_guard<std::mutex> lock(g_filterMutex);
if (g_filtersLoaded) {
activeSites = g_junkSites;
activeTlds = g_tlds;
activeTags = g_truncateTags;
activeWords = g_junkWords;
} else {
activeSites = {
"olamovies", "vegamovies", "moviesmod", "katmoviehd", "mkvcinemas",
"filmyzilla", "filmywap", "1tamilmv", "jiorockers", "ibomma", "yts",
"yify", "psa", "qxr", "tigole", "rarbg", "pahe", "pagalworld", "mrjatt",
"djpunjab", "wapking", "songspk", "djmaza", "pendujatt", "naasongs",
"masstamilan", "jiosaavn", "moviesverse"
};
activeTlds = {
".top", ".com", ".net", ".org", ".in", ".nl", ".is", ".to", ".pw",
".cc", ".site", ".info", ".biz", ".co", ".nz", ".uk", ".mx", ".ws", ".pro"
};
activeTags = {
"2160p", "1080p", "720p", "480p", "4k", "bluray", "web-dl", "webrip", "hdrip", "camrip", "brrip"
};
activeWords = {
"downloaded from", "download from", "shared by", "brought to you by", "visit website",
"downloaded", "download", "320kbps", "128kbps", "kbps", "official video", "lyric video",
"ringtone", "full song", "pagalworld", "mrjatt",
"djpunjab", "wapking", "songspk", "djmaza", "pendujatt", "naasongs", "masstamilan",
"jiosaavn", "olamovies", "uhdmovies", "vegamovies", "moviesmod", "katmoviehd", "mkvcinemas",
"filmyzilla", "filmywap", "1tamilmv", "jiorockers", "ibomma", "yts", "yify", "psa", "qxr",
"tigole", "rarbg", "pahe", "x264", "x265", "hevc", "10bit"
};
}
}
activeSites.insert(activeSites.end(), customJunk.begin(), customJunk.end());
for (const auto& site : activeSites) {
if (site.empty()) continue;
for (const auto& tld : activeTlds) {
std::string url = site + tld;
size_t pos;
while ((pos = lowerText.find(url)) != std::string::npos) {
noBrackets.replace(pos, url.length(), std::string(url.length(), ' '));
lowerText.replace(pos, url.length(), std::string(url.length(), ' '));
}
}
}
for (char &c : noBrackets) {
if (c == '.' || c == '_' || c == '~') c = ' ';
}
lowerText = noBrackets;
std::transform(lowerText.begin(), lowerText.end(), lowerText.begin(), ::tolower);
for (const auto& tag : activeTags) {
size_t pos = lowerText.find(tag);
if (pos != std::string::npos) {
noBrackets = noBrackets.substr(0, pos);
lowerText = lowerText.substr(0, pos);
}
}
activeWords.insert(activeWords.end(), customJunk.begin(), customJunk.end());
std::string result = noBrackets;
for (const auto& word : activeWords) {
if (word.empty()) continue;
size_t pos;
while (true) {
lowerText = result;
std::transform(lowerText.begin(), lowerText.end(), lowerText.begin(), ::tolower);
pos = lowerText.find(word);
if (pos == std::string::npos) break;
result.replace(pos, word.length(), " ");
}
}
std::string finalClean;
for (char c : result) {
if (c == ' ' && !finalClean.empty() && finalClean.back() == ' ') continue;
finalClean += c;
}
size_t start = finalClean.find_first_not_of(" -");
if (start == std::string::npos) return "";
size_t end = finalClean.find_last_not_of(" -");
return finalClean.substr(start, end - start + 1);
}
std::string ExtractYear(const std::string& filename) {
for (size_t i = 0; i + 3 < filename.length(); i++) {
if ((filename[i] == '1' && filename[i+1] == '9') ||
(filename[i] == '2' && filename[i+1] == '0')) {
if (isdigit(filename[i+2]) && isdigit(filename[i+3])) {
bool validStart = (i == 0 || !isdigit(filename[i-1]));
bool validEnd = (i + 4 == filename.length() || !isdigit(filename[i+4]));
if (validStart && validEnd) {
return filename.substr(i, 4);
}
}
}
}
return "";
}
// =============================================================
// 2. ASYNC NETWORK & IMAGE API LOGIC
// =============================================================
std::string FetchHttps(const std::wstring& host, const std::wstring& path) {
std::string result = "";
HINTERNET hSession = WinHttpOpen(L"VLC-RPC-Mod", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
if (!hSession) return "";
WinHttpSetTimeouts(hSession, 3000, 3000, 3000, 3000);
HINTERNET hConnect = WinHttpConnect(hSession, host.c_str(), INTERNET_DEFAULT_HTTPS_PORT, 0);
if (hConnect) {
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", path.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);
if (hRequest) {
std::wstring headers = L"User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n";
if (WinHttpSendRequest(hRequest, headers.c_str(), headers.length(), WINHTTP_NO_REQUEST_DATA, 0, 0, 0) &&
WinHttpReceiveResponse(hRequest, NULL)) {
DWORD dwSize = 0;
DWORD dwDownloaded = 0;
do {
if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) break;
if (dwSize == 0) break;
std::vector<char> buffer(dwSize + 1);
if (WinHttpReadData(hRequest, buffer.data(), dwSize, &dwDownloaded)) {
result.append(buffer.data(), dwDownloaded);
}
} while (dwSize > 0);
}
WinHttpCloseHandle(hRequest);
}
WinHttpCloseHandle(hConnect);
}
WinHttpCloseHandle(hSession);
return result;
}
std::string FindExternalArtwork(int type, const std::string& queryTitle, const std::string& querySub, bool isTvShow = false) {
if (queryTitle.empty()) return "";
std::string finalUrl = "";
std::string suffix = (type == 2) ? " song cover art" : (isTvShow ? " show poster" : " movie poster");
std::string term = UrlEncode(queryTitle + " " + querySub + suffix);
std::wstring host = L"www.bing.com";
std::wstring path = StrToWStr("/images/async?q=" + term + "&first=0&count=1&adlt=off");
std::string html = FetchHttps(host, path);
std::string searchToken = ""turl":"";
size_t start = html.find(searchToken);
if (start == std::string::npos) {
searchToken = "\"turl\":\"";
start = html.find(searchToken);
}
if (start != std::string::npos) {
std::string idToken = "id=OIP.";
size_t idStart = html.find(idToken, start);
if (idStart != std::string::npos && idStart < start + 500) {
idStart += 3;
size_t endAmp = html.find("&", idStart);
size_t endQuote = html.find("\"", idStart);
size_t end = std::min(endAmp, endQuote);
if (end != std::string::npos) {
std::string imageId = html.substr(idStart, end - idStart);
finalUrl = "https://tse1.mm.bing.net/th?id=" + imageId;
Wh_Log(L"[VLC-RPC] Found external artwork via async: %S", finalUrl.c_str());
}
}
}
return finalUrl;
}
bool ReadFileBytes(const std::wstring& path, std::vector<char>& data) {
std::ifstream file(path.c_str(), std::ios::binary | std::ios::ate);
if (!file) return false;
std::streamsize size = file.tellg();
if (size <= 0 || size > 10 * 1024 * 1024) return false;
file.seekg(0, std::ios::beg);
data.resize(size);
if (!file.read(data.data(), size)) return false;
return true;
}
std::string UploadToUguu(const std::string& fileUrl) {
std::string pathStr = UrlDecode(fileUrl);
size_t filePrefix = pathStr.find("file:///");
if (filePrefix != std::string::npos) pathStr = pathStr.substr(filePrefix + 8);
for (auto &c : pathStr) if (c == '/') c = '\\';
std::vector<char> fileData;
if (!ReadFileBytes(StrToWStr(pathStr), fileData)) return "";
HINTERNET hSession = WinHttpOpen(L"VLC-RPC-Mod", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
if (!hSession) return "";
HINTERNET hConnect = WinHttpConnect(hSession, L"uguu.se", INTERNET_DEFAULT_HTTPS_PORT, 0);
if (!hConnect) { WinHttpCloseHandle(hSession); return ""; }
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"POST", L"/upload.php", NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE);
if (!hRequest) { WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession); return ""; }
std::string boundary = "------------------------VlcRpcModBoundary";
std::string header = "Content-Type: multipart/form-data; boundary=" + boundary;
std::string bodyHead;
bodyHead += "--" + boundary + "\r\n";
bodyHead += "Content-Disposition: form-data; name=\"files[]\"; filename=\"cover.jpg\"\r\n\r\n";
std::string bodyTail;
bodyTail += "\r\n--" + boundary + "--\r\n";
DWORD totalSize = (DWORD)(bodyHead.size() + fileData.size() + bodyTail.size());
bool success = false;
std::string resultUrl = "";
if (WinHttpAddRequestHeaders(hRequest, StrToWStr(header).c_str(), (DWORD)-1L, WINHTTP_ADDREQ_FLAG_ADD) &&
WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, totalSize, 0)) {
DWORD bytesWritten;
WinHttpWriteData(hRequest, bodyHead.c_str(), (DWORD)bodyHead.size(), &bytesWritten);
WinHttpWriteData(hRequest, fileData.data(), (DWORD)fileData.size(), &bytesWritten);
WinHttpWriteData(hRequest, bodyTail.c_str(), (DWORD)bodyTail.size(), &bytesWritten);
if (WinHttpReceiveResponse(hRequest, NULL)) {
DWORD dwSize = 0, dwDownloaded = 0;
do {
if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) break;
if (dwSize == 0) break;
std::vector<char> respBuf(dwSize + 1);
if (WinHttpReadData(hRequest, respBuf.data(), dwSize, &dwDownloaded)) {
resultUrl.append(respBuf.data(), dwDownloaded);
}
} while (dwSize > 0);
while (!resultUrl.empty() && (resultUrl.back() == '\n' || resultUrl.back() == '\r')) resultUrl.pop_back();
std::string extractedUrl = "";
size_t urlPos = resultUrl.find("\"url\"");
if (urlPos != std::string::npos) {
size_t colonPos = resultUrl.find(":", urlPos);
if (colonPos != std::string::npos) {
size_t quote1 = resultUrl.find("\"", colonPos);
if (quote1 != std::string::npos) {
size_t quote2 = resultUrl.find("\"", quote1 + 1);
if (quote2 != std::string::npos) {
extractedUrl = resultUrl.substr(quote1 + 1, quote2 - quote1 - 1);
}
}
}
}
if (!extractedUrl.empty() && extractedUrl.find("http") == 0) {
resultUrl = extractedUrl;
success = true;
} else if (resultUrl.find("http") == 0) {
success = true;
}
}
}
WinHttpCloseHandle(hRequest); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hSession);
if (success) {
std::string finalCleanUrl;
for (char c : resultUrl) { if (c != '\\') finalCleanUrl += c; }
Wh_Log(L"[VLC-RPC] Uploaded local cover art to Uguu: %S", finalCleanUrl.c_str());
return finalCleanUrl;
}
return "";
}
// =============================================================
// 3. LOGIC HELPERS
// =============================================================
std::string GetAudioLanguages(const std::string& json) {
std::vector<std::string> activeLangs;
std::vector<std::string> allLangs;
for (int i = 0; i < 60; i++) {
std::string streamKey = "\"Stream " + std::to_string(i) + "\":{";
size_t start = json.find(streamKey);
if (start == std::string::npos) continue;
size_t end = json.find("}", start);
if (end == std::string::npos) continue;
std::string block = json.substr(start, end - start);
if (block.find("\"Type\":\"Audio\"") != std::string::npos) {
std::string lang = ExtractString(block, "Language");
if (!lang.empty()) {
std::string shortLang = lang.substr(0, 2);
if (shortLang[0] >= 'a' && shortLang[0] <= 'z') shortLang[0] -= 32;
if (shortLang[1] >= 'a' && shortLang[1] <= 'z') shortLang[1] -= 32;
bool existsAll = false;
for (const auto& l : allLangs) if (l == shortLang) existsAll = true;
if (!existsAll) allLangs.push_back(shortLang);
bool isDecoded = (block.find("Decoded_format") != std::string::npos) ||
(block.find("Decoded_channels") != std::string::npos);
if (isDecoded) {
bool existsActive = false;
for (const auto& l : activeLangs) if (l == shortLang) existsActive = true;
if (!existsActive) activeLangs.push_back(shortLang);
}
}
}
}
std::vector<std::string>* targetList = (activeLangs.size() > 0) ? &activeLangs : &allLangs;
std::string result = "";
for (size_t i = 0; i < targetList->size(); i++) {
if (i > 0) result += " | ";
result += (*targetList)[i];
}
return result;
}
std::string GetQualityTags(const std::string& json) {
std::string tags = "";
for (int i = 0; i < 10; i++) {
std::string streamKey = "\"Stream " + std::to_string(i) + "\":{";
size_t start = json.find(streamKey);
if (start == std::string::npos) continue;
size_t end = json.find("}", start);
if (end == std::string::npos) continue;
std::string block = json.substr(start, end - start);
if (block.find("\"Type\":\"Video\"") != std::string::npos) {
std::string res = ExtractString(block, "Video_resolution");
if (!res.empty()) {
size_t xPos = res.find("x");
if (xPos != std::string::npos) {
try {
long long width = std::stoll(res.substr(0, xPos));
if (width >= 3800) tags = "4K";
else if (width >= 2500) tags = "2K";
else if (width >= 1900) tags = "1080p";
else if (width >= 1200) tags = "720p";
else tags = "SD";
} catch(...) {}
}
}
std::string color = ExtractString(block, "Color_primaries");
std::string transfer = ExtractString(block, "Color_transfer_function");
bool isHDR = false;
if (color.find("2020") != std::string::npos) isHDR = true;
if (transfer.find("PQ") != std::string::npos) isHDR = true;
if (transfer.find("HLG") != std::string::npos) isHDR = true;
if (transfer.find("2084") != std::string::npos) isHDR = true;
if (isHDR) {
if (!tags.empty()) tags += SEP;
tags += "HDR";
}
break;
}
}
return tags;
}
std::string GenerateButtonUrl(std::string query, const std::string& provider, const std::string& customUrl) {
std::string base = "";
if (provider == "Google") base = "https://www.google.com/search?q=";
else if (provider == "Bing") base = "https://www.bing.com/search?q=";
else if (provider == "IMDb") base = "https://www.imdb.com/find/?q=";
else if (provider == "YouTube") base = "https://www.youtube.com/results?search_query=";
else if (provider == "Custom") base = customUrl;
if (base.empty()) base = "https://www.google.com/search?q=";
if (query.empty()) query = "VLC Media Player";
return base + UrlEncode(query);
}
int DetectActivityType(const std::string& filename, const std::string& quality) {
if (!quality.empty()) return 3; // Watching
std::string ext = "";
size_t dot = filename.rfind(".");
if (dot != std::string::npos) {
ext = filename.substr(dot);
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
}
if (ext == ".mp3" || ext == ".flac" || ext == ".wav" || ext == ".m4a" || ext == ".aac" || ext == ".ogg" || ext == ".wma" || ext == ".opus") {
return 2; // Listening
}
if (ext == ".mkv" || ext == ".mp4" || ext == ".avi" || ext == ".mov" || ext == ".wmv" || ext == ".webm" || ext == ".m4v") {
return 3; // Watching
}
return 0; // Playing
}
// =============================================================
// 4. REMOTE FILTER UPDATER & CACHING
// =============================================================
std::wstring GetCacheFilePathW() {
WCHAR storagePath[MAX_PATH];
if (Wh_GetModStoragePath(storagePath, ARRAYSIZE(storagePath))) {
return std::wstring(storagePath) + L"\\junklist.txt";
}
return L"";
}
bool IsCacheValid(const std::wstring& path) {
struct _stat result;
if (_wstat(path.c_str(), &result) == 0) {
time_t now = time(nullptr);
if (difftime(now, result.st_mtime) < 21600) {
return true;
}
}
return false;
}
struct FilterData {
std::vector<std::string> sites;
std::vector<std::string> tlds;
std::vector<std::string> tags;
std::vector<std::string> words;
bool isValid = false;
};
FilterData ParseFilters(const std::string& text) {
FilterData data;
if (text.empty()) return data;
std::stringstream ss(text);
std::string line;
int currentSection = 0;
while (std::getline(ss, line)) {
if (!line.empty() && line.back() == '\r') line.pop_back();
if (line.empty()) continue;
size_t first = line.find_first_not_of(" \t");
if (first == std::string::npos) continue;
size_t last = line.find_last_not_of(" \t");
line = line.substr(first, last - first + 1);
if (line == "[SITES]") { currentSection = 1; continue; }
if (line == "[TLDS]") { currentSection = 2; continue; }
if (line == "[SCENE]") { currentSection = 3; continue; }
if (line == "[WORDS]") { currentSection = 4; continue; }
std::string lowerLine = line;
std::transform(lowerLine.begin(), lowerLine.end(), lowerLine.begin(), ::tolower);
if (currentSection == 1) data.sites.push_back(lowerLine);
else if (currentSection == 2) data.tlds.push_back(lowerLine);
else if (currentSection == 3) data.tags.push_back(lowerLine);
else if (currentSection == 4) data.words.push_back(lowerLine);
}
if (!data.sites.empty() && !data.tlds.empty() && !data.tags.empty() && !data.words.empty()) {
data.isValid = true;
}
return data;
}
FilterData LoadFiltersFromFile(const std::wstring& path) {
std::ifstream file(path.c_str());
if (!file.is_open()) return FilterData();
std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
return ParseFilters(content);
}
void FetchRemoteFilters(bool strictLocalMode) {
std::wstring cachePath = GetCacheFilePathW();
if (strictLocalMode) return;
if (!cachePath.empty() && IsCacheValid(cachePath)) {
FilterData localData = LoadFiltersFromFile(cachePath);
if (localData.isValid) {
std::lock_guard<std::mutex> lock(g_filterMutex);
g_junkSites = localData.sites;
g_tlds = localData.tlds;
g_truncateTags = localData.tags;
g_junkWords = localData.words;
g_filtersLoaded = true;
Wh_Log(L"[VLC-RPC] Loaded filter cache from disk (%s).", cachePath.c_str());
return;
}
}
PCWSTR url = L"https://raw.githubusercontent.com/ciizerr/vlc-discord-rpc-archive/main/assets/filters.txt";
const WH_URL_CONTENT* content = Wh_GetUrlContent(url, nullptr);
if (content) {
std::string result(content->data, content->length);
Wh_FreeUrlContent(content);
FilterData remoteData = ParseFilters(result);
if (remoteData.isValid) {
if (!cachePath.empty()) {
std::ofstream out(cachePath.c_str(), std::ios::trunc);
if (out.is_open()) {
out << result;
out.close();
}
}
std::lock_guard<std::mutex> lock(g_filterMutex);
g_junkSites = remoteData.sites;
g_tlds = remoteData.tlds;
g_truncateTags = remoteData.tags;
g_junkWords = remoteData.words;
g_filtersLoaded = true;
Wh_Log(L"[VLC-RPC] Downloaded and applied updated filters from GitHub.");
return;
}
}
if (!cachePath.empty()) {
FilterData staleData = LoadFiltersFromFile(cachePath);
if (staleData.isValid) {
std::lock_guard<std::mutex> lock(g_filterMutex);
g_junkSites = staleData.sites;
g_tlds = staleData.tlds;
g_truncateTags = staleData.tags;
g_junkWords = staleData.words;
g_filtersLoaded = true;
}
}
}
// =============================================================
// 5. NOTIFICATIONS
// =============================================================
void ShowSystemToast(const std::wstring& title, const std::wstring& message, const std::string& imageUrl) {
std::thread([title, message, imageUrl]() {
HICON hDynamicIcon = NULL;
if (!imageUrl.empty() && imageUrl.find("http") == 0) {
std::wstring wideUrl = StrToWStr(imageUrl);
const WH_URL_CONTENT* content = Wh_GetUrlContent(wideUrl.c_str(), nullptr);
if (content && content->length > 0) {
IStream* pStream = nullptr;
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, content->length);
if (hMem) {
void* pData = GlobalLock(hMem);
memcpy(pData, content->data, content->length);
GlobalUnlock(hMem);
CreateStreamOnHGlobal(hMem, TRUE, &pStream);
}
if (pStream) {
Gdiplus::Bitmap* sourceBmp = Gdiplus::Bitmap::FromStream(pStream);
if (sourceBmp && sourceBmp->GetLastStatus() == Gdiplus::Ok) {
UINT w = sourceBmp->GetWidth();
UINT h = sourceBmp->GetHeight();
UINT size = (w < h) ? w : h;
Gdiplus::Bitmap* squareBmp = new Gdiplus::Bitmap(size, size, PixelFormat32bppARGB);
if (squareBmp && squareBmp->GetLastStatus() == Gdiplus::Ok) {
Gdiplus::Graphics graphics(squareBmp);
graphics.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
UINT x = (w > h) ? (w - h) / 2 : 0;
UINT y = (h > w) ? (h - w) / 2 : 0;
graphics.DrawImage(sourceBmp,
Gdiplus::Rect(0, 0, size, size),
x, y, size, size,
Gdiplus::UnitPixel);
squareBmp->GetHICON(&hDynamicIcon);
}
delete squareBmp;
}
delete sourceBmp;
pStream->Release();
}
Wh_FreeUrlContent(content);
}
}
WCHAR storagePath[MAX_PATH];
std::wstring defaultIconPath = L"";
if (Wh_GetModStoragePath(storagePath, ARRAYSIZE(storagePath))) {
defaultIconPath = std::wstring(storagePath) + L"\\vlc-discord-icon.ico";
struct _stat result;
if (_wstat(defaultIconPath.c_str(), &result) != 0) {
const WH_URL_CONTENT* content = Wh_GetUrlContent(L"https://raw.githubusercontent.com/ciizerr/vlc-discord-rpc-archive/main/assets/vlc-discord-icon.ico", nullptr);
if (content && content->length > 0) {
std::ofstream out(defaultIconPath.c_str(), std::ios::binary | std::ios::trunc);
if (out.is_open()) {
out.write((const char*)content->data, content->length);
out.close();
}
}
if (content) Wh_FreeUrlContent(content);
}
}
HWND hwnd = CreateWindowExW(0, L"STATIC", L"VLC-RPC-Toast", WS_OVERLAPPED, 0, 0, 0, 0, NULL, NULL, NULL, NULL);
if (!hwnd) {
if (hDynamicIcon) DestroyIcon(hDynamicIcon);
return;
}
HICON hAppIcon = NULL;
if (!defaultIconPath.empty()) {
hAppIcon = (HICON)LoadImageW(NULL, defaultIconPath.c_str(), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_LOADFROMFILE);
}
if (!hAppIcon) hAppIcon = LoadIcon(NULL, IDI_INFORMATION);
SendMessageW(hwnd, WM_SETICON, ICON_SMALL, (LPARAM)hAppIcon);
SendMessageW(hwnd, WM_SETICON, ICON_BIG, (LPARAM)hAppIcon);
NOTIFYICONDATAW nid = {0};
nid.cbSize = sizeof(NOTIFYICONDATAW);
nid.hWnd = hwnd;
nid.uID = 1338;
nid.uFlags = NIF_ICON | NIF_TIP | NIF_INFO;
nid.hIcon = hAppIcon;
wcscpy_s(nid.szTip, L"VLC Discord RPC");
wcsncpy_s(nid.szInfoTitle, title.c_str(), 63);
wcsncpy_s(nid.szInfo, message.c_str(), 255);
if (hDynamicIcon) {
nid.dwInfoFlags = NIIF_USER | NIIF_LARGE_ICON | NIIF_RESPECT_QUIET_TIME;
nid.hBalloonIcon = hDynamicIcon;
} else {
nid.dwInfoFlags = NIIF_USER | NIIF_RESPECT_QUIET_TIME;
}
Shell_NotifyIconW(NIM_ADD, &nid);
DWORD start = GetTickCount();
MSG msg;
while (GetTickCount() - start < 5000) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
Sleep(50);
}
}
Shell_NotifyIconW(NIM_DELETE, &nid);
if (hAppIcon) DestroyIcon(hAppIcon);
if (hDynamicIcon) DestroyIcon(hDynamicIcon);
DestroyWindow(hwnd);
}).detach();
}
// =============================================================
// 6. MAIN WORKER
// =============================================================
void Worker() {
bool bStrictLocalMode = Wh_GetIntSetting(L"StrictLocalMode");
FetchRemoteFilters(bStrictLocalMode);
std::string defaultId = "1465711556418474148";
PCWSTR sId = Wh_GetStringSetting(L"ClientId");
std::string myClientId = sId ? WStrToStr(sId) : defaultId;
if (myClientId.empty()) myClientId = defaultId;
Wh_FreeStringSetting(sId);
bool bShowCoverArt = Wh_GetIntSetting(L"ShowCoverArt");
bool bShowQualityTags = Wh_GetIntSetting(L"ShowQualityTags");
bool bShowChapter = Wh_GetIntSetting(L"ShowChapter");
bool bShowAudioLanguage = Wh_GetIntSetting(L"ShowAudioLanguage");
bool bEnableMetadataCleaner = Wh_GetIntSetting(L"EnableMetadataCleaner");
bool bMinimalMode = Wh_GetIntSetting(L"MinimalMode");
bool bShowNotifications = Wh_GetIntSetting(L"ShowNotifications");
PCWSTR sJunk = Wh_GetStringSetting(L"CustomJunkWords");
std::string customJunkStr = sJunk ? WStrToStr(sJunk) : "";
Wh_FreeStringSetting(sJunk);
std::vector<std::string> customJunkList;
std::stringstream ss(customJunkStr);
std::string token;
while (std::getline(ss, token, ',')) {
size_t start = token.find_first_not_of(" ");
if (start != std::string::npos) {
token = token.substr(start, token.find_last_not_of(" ") - start + 1);
std::transform(token.begin(), token.end(), token.begin(), ::tolower);
customJunkList.push_back(token);
}
}
PCWSTR sTheme = Wh_GetStringSetting(L"Theme");
std::string myTheme = sTheme ? WStrToStr(sTheme) : "";
Wh_FreeStringSetting(sTheme);
std::string assetLarge = myTheme + "vlc_icon";
std::string assetPlay = myTheme + "play_icon";
std::string assetPause = myTheme + "pause_icon";
std::string assetStop = myTheme + "stop_icon";
PCWSTR sProv = Wh_GetStringSetting(L"Provider");
std::string myProvider = sProv ? WStrToStr(sProv) : "Google";
Wh_FreeStringSetting(sProv);
PCWSTR sCust = Wh_GetStringSetting(L"CustomUrl");
std::string myCustomUrl = sCust ? WStrToStr(sCust) : "";
Wh_FreeStringSetting(sCust);
PCWSTR sLbl = Wh_GetStringSetting(L"ButtonLabel");
std::string myBtnLabel = sLbl ? WStrToStr(sLbl) : "Search This";
Wh_FreeStringSetting(sLbl);
myBtnLabel = SanitizeString(myBtnLabel);
PCWSTR sMusicName = Wh_GetStringSetting(L"MusicActivityName");
std::string myMusicNameSetting = sMusicName ? WStrToStr(sMusicName) : "Title";
Wh_FreeStringSetting(sMusicName);
int vlcPort = 0;
std::wstring vlcAuthBase64 = L"OjEyMzQ=";
std::string vlcRawPassword = "";
ReadVlcConfig(vlcPort, vlcAuthBase64, vlcRawPassword);
int customPortSetting = Wh_GetIntSetting(L"CustomPort");
std::vector<int> candidatePorts;
if (customPortSetting > 0) candidatePorts.push_back(customPortSetting);
if (vlcPort > 0 && std::find(candidatePorts.begin(), candidatePorts.end(), vlcPort) == candidatePorts.end()) candidatePorts.push_back(vlcPort);
int fallbacks[] = {8080, 9080, 9090, 7080, 4212, 8081, 8088};
for (int p : fallbacks) {
if (std::find(candidatePorts.begin(), candidatePorts.end(), p) == candidatePorts.end()) {
candidatePorts.push_back(p);
}
}
int currentPortIndex = 0;
Wh_Log(L"[VLC-RPC] Worker started. Port=%d, Password=%S", vlcPort, vlcRawPassword.c_str());
HINTERNET hSession = WinHttpOpen(L"VLC-RPC/1.5", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
HINTERNET hConnect = NULL;
HINTERNET hRequest = NULL;
HANDLE hPipe = INVALID_HANDLE_VALUE;
bool isConnected = false;
std::string lastTop = ""; std::string lastBot = ""; bool lastPlaying = false;
std::string lastState = ""; int heartbeat = 0;
long long anchorStart = 0; long long anchorEnd = 0;
int lastActivityType = 0;
std::string lastDisplayImage = "";
std::string lastRawFilename = "";
std::string lastRawTitle = "";
std::string lastRawArtist = "";
std::string lastShowName = "";
std::string cachedFilename = "";
std::string cachedTitle = "";
std::string cachedArtist = "";
std::string cachedShowName = "";
std::string lastToastMediaKey = "";
int toastTimer = 0;
bool toastFired = false;
bool logged401 = false;
bool logged200 = false;
bool loggedFailure = false;
while (!g_stopThread.load()) {
int activePort = candidatePorts[currentPortIndex];
if (hSession && !hConnect) {
hConnect = WinHttpConnect(hSession, L"127.0.0.1", activePort, 0);
}
if (hConnect) hRequest = WinHttpOpenRequest(hConnect, L"GET", L"/requests/status.json", NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
bool requestSuccess = false;
if (hRequest) {
std::wstring headers = L"Authorization: Basic " + vlcAuthBase64;
if (WinHttpSendRequest(hRequest, headers.c_str(), headers.length(), WINHTTP_NO_REQUEST_DATA, 0, 0, 0) &&
WinHttpReceiveResponse(hRequest, NULL)) {
DWORD statusCode = 0;
DWORD dwSizeStatus = sizeof(statusCode);
WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &dwSizeStatus, WINHTTP_NO_HEADER_INDEX);
if (statusCode == 401) {
if (!logged401) {
Wh_Log(L"[VLC-RPC] Auth failed (401). Check the Lua HTTP password in %%APPDATA%%\\vlc\\vlcrc.");
logged401 = true;
}
} else if (statusCode == 200) {
if (!logged200) {
Wh_Log(L"[VLC-RPC] Connected to VLC on port %d.", activePort);
logged200 = true;
}
}
requestSuccess = true;
std::string json; DWORD dwSize = 0, dwDownloaded = 0;
do {
if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) break;
if (dwSize == 0) break;
std::vector<char> buffer(dwSize + 1);
if (WinHttpReadData(hRequest, buffer.data(), dwSize, &dwDownloaded)) {
json.append(buffer.data(), dwDownloaded);
}
} while (dwSize > 0);
if (!json.empty()) {
std::string stateStr = ExtractString(json, "state");
if (stateStr == "stopped") {
if (lastState != "stopped") {
if (!isConnected || hPipe == INVALID_HANDLE_VALUE) {
for (int i=0; i<10; i++) {
std::string name = "\\\\.\\pipe\\discord-ipc-" + std::to_string(i);
hPipe = CreateFileA(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hPipe != INVALID_HANDLE_VALUE) {
Wh_Log(L"[VLC-RPC] Connected to Discord IPC (pipe %d).", i);
break;
}
}
if (hPipe != INVALID_HANDLE_VALUE) {
std::string hs = "{\"v\":1,\"client_id\":\"" + myClientId + "\"}";
int op=0; int l=(int)hs.length(); DWORD w; WriteFile(hPipe,&op,4,&w,NULL); WriteFile(hPipe,&l,4,&w,NULL); WriteFile(hPipe,hs.c_str(),l,&w,NULL);
isConnected = true;
} else {
static bool loggedIpcFail1 = false;
if (!loggedIpcFail1) {
Wh_Log(L"[VLC-RPC] Could not connect to Discord IPC. Is Discord running?");
loggedIpcFail1 = true;
}
}
}
if (isConnected) {
std::string js = "{\"cmd\":\"SET_ACTIVITY\",\"args\":{\"pid\":" + NumToStr(GetCurrentProcessId()) + ",\"activity\":{";
js += "\"details\":\"Idling\",\"state\":\"Waiting for media...\",\"type\":0,";
js += "\"assets\":{\"large_image\":\"" + assetLarge + "\",\"large_text\":\"VLC Media Player\"";
if (!bMinimalMode) {
js += ",\"small_image\":\"" + assetStop + "\",\"small_text\":\"Stopped\"";
}
js += "}";
js += "}},\"nonce\":\"1\"}";
int op=1; int l=(int)js.length(); DWORD w; WriteFile(hPipe,&op,4,&w,NULL); WriteFile(hPipe,&l,4,&w,NULL); WriteFile(hPipe,js.c_str(),l,&w,NULL);
}
lastTop = ""; lastState = "stopped";
}
}
else if (stateStr == "playing" || stateStr == "paused") {
std::string rawFilename = CleanString(ExtractString(json, "filename"));
if (rawFilename != lastToastMediaKey) {
lastToastMediaKey = rawFilename;
toastTimer = 0;
toastFired = false;
}
if (stateStr == "playing" && !toastFired) {
toastTimer++;
}
std::string rawTitle = ExtractString(json, "title");
std::string showName = ExtractString(json, "showName");
std::string season = ExtractString(json, "seasonNumber");
std::string episode = ExtractString(json, "episodeNumber");
std::string rawArtist = ExtractString(json, "artist");
std::string album = ExtractString(json, "album");
std::string artworkUrl = ExtractString(json, "artwork_url");
std::string date = ExtractString(json, "date");
if (date.empty()) date = ExtractYear(rawFilename);
long long chapter = ExtractNumber(json, "chapter");
long long time = ExtractNumber(json, "time");
long long length = ExtractNumber(json, "length");
bool isPlaying = (stateStr == "playing");
std::string quality = GetQualityTags(json);
std::string audio = GetAudioLanguages(json);
int activityType = DetectActivityType(rawFilename, quality);
if (rawFilename != lastRawFilename || rawTitle != lastRawTitle || rawArtist != lastRawArtist || showName != lastShowName) {
Wh_Log(L"[VLC-RPC] Media changed: %S", rawFilename.c_str());
std::string filenameClean = bEnableMetadataCleaner ? CleanMetadata(rawFilename, customJunkList) : rawFilename;
cachedFilename = filenameClean.empty() ? rawFilename : filenameClean;
std::string titleClean = bEnableMetadataCleaner ? CleanMetadata(rawTitle, customJunkList) : rawTitle;
cachedTitle = titleClean.empty() ? cachedFilename : titleClean;
std::string artistClean = bEnableMetadataCleaner ? CleanMetadata(rawArtist, customJunkList) : rawArtist;
cachedArtist = artistClean.empty() ? rawArtist : artistClean;
std::string showClean = bEnableMetadataCleaner ? CleanMetadata(showName, customJunkList) : showName;
cachedShowName = showClean.empty() ? showName : showClean;
lastRawFilename = rawFilename;
lastRawTitle = rawTitle;
lastRawArtist = rawArtist;
lastShowName = showName;
}
std::string filename = cachedFilename;
std::string title = cachedTitle;
std::string artist = cachedArtist;
std::string top = ""; std::string bot = ""; std::string query = "";
std::string activityName = "";
std::string largeText = "VLC Media Player";
if (activityType == 2) {
std::string defaultName = title.empty() ? filename : title;
if (myMusicNameSetting == "Artist" && !artist.empty()) {
activityName = artist;
} else if (myMusicNameSetting == "Album" && !album.empty()) {
activityName = album;
} else {
activityName = defaultName;
}
top = defaultName;
query = defaultName + " " + artist;
if (!artist.empty()) {
bot = "by " + artist;
} else if (!album.empty()) {
bot = album;
} else {
bot = "Music";
}
if (!album.empty()) largeText = album;
else largeText = "Listening to Music";
} else {
if (!showName.empty() && !episode.empty()) {
activityName = cachedShowName;
top = activityName;
if (bShowQualityTags && !quality.empty()) top += SEP + quality;
bot = "S" + season + " E" + episode;
if (bShowChapter && chapter >= 0) bot += SEP + "Ch " + NumToStr(chapter + 1);
if (bShowAudioLanguage && !audio.empty()) bot += SEP + audio;
query = activityName + " S" + season + " E" + episode;
largeText = "Watching TV Show";
}
else if (!title.empty()) {
activityName = title;
top = title;
if (bShowQualityTags && !quality.empty()) top += SEP + quality;
bot = "";
if (bShowChapter && chapter >= 0) bot = "Ch " + NumToStr(chapter + 1);
if (bShowAudioLanguage && !audio.empty()) {
if (!bot.empty()) bot += SEP;
bot += audio;
}
query = title;
largeText = "Watching Movie";
}
else {
activityName = filename;
top = filename;
if (bShowQualityTags && !quality.empty()) top += SEP + quality;
bot = "";
query = filename;
largeText = "Watching Video";
}
}
if (activityName.empty()) activityName = "VLC Media Player";
std::string displayImage = assetLarge;
std::string targetCacheKey = "";
if (bShowCoverArt) {
bool isUpload = false;
if (!artworkUrl.empty() && artworkUrl.find("file://") == 0) {
targetCacheKey = UrlDecode(artworkUrl);
isUpload = true;
} else if (activityType == 2 || activityType == 3) {
std::string queryTitle = (activityType == 2) ? (title.empty() ? filename : title) : activityName;
std::string querySub = (activityType == 2) ? artist : date;
targetCacheKey = "EXT_" + NumToStr(activityType) + "_" + queryTitle + querySub + (!showName.empty() && !episode.empty() ? "_TV" : "");
}
if (!targetCacheKey.empty()) {
bool foundInCache = false;
{
std::lock_guard<std::mutex> lock(g_cacheMutex);
if (g_imageCache.find(targetCacheKey) != g_imageCache.end()) {
displayImage = g_imageCache[targetCacheKey];
if (displayImage.empty() || displayImage.find("http") != 0) displayImage = assetLarge;
foundInCache = true;
}
}
if (!foundInCache) {
bool alreadyFetching = false;
{
std::lock_guard<std::mutex> lock(g_cacheMutex);
if (g_fetchInProgress[targetCacheKey]) alreadyFetching = true;
else g_fetchInProgress[targetCacheKey] = true;
}
if (!alreadyFetching) {
if (isUpload) {
std::string artUrl = artworkUrl;
std::thread([targetCacheKey, artUrl]() {
std::string result = UploadToUguu(artUrl);
std::lock_guard<std::mutex> lock(g_cacheMutex);
g_imageCache[targetCacheKey] = result;
g_fetchInProgress[targetCacheKey] = false;
}).detach();
} else {
int aType = activityType;
std::string qTitle = (activityType == 2) ? (title.empty() ? filename : title) : activityName;
std::string qSub = (activityType == 2) ? artist : date;
bool isTv = (!showName.empty() && !episode.empty());
std::thread([targetCacheKey, aType, qTitle, qSub, isTv]() {
std::string result = FindExternalArtwork(aType, qTitle, qSub, isTv);
std::lock_guard<std::mutex> lock(g_cacheMutex);
g_imageCache[targetCacheKey] = result;
g_fetchInProgress[targetCacheKey] = false;
}).detach();
}
}
}
}
}
auto now = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch()).count();
long long cStart = now - (time * 1000);
long long cEnd = cStart + (length * 1000);
long long drift = cStart - anchorStart;
if (drift < 0) drift = -drift;
bool textChg = (top != lastTop || bot != lastBot);
bool stateChg = (isPlaying != lastPlaying);
bool typeChg = (activityType != lastActivityType);
bool artChg = (displayImage != lastDisplayImage);
bool majorDrift = (drift > 3000);
bool force = (heartbeat > 30);
if (textChg || stateChg || typeChg || artChg || majorDrift || force) {
anchorStart = cStart; anchorEnd = cEnd;
if (!isConnected || hPipe == INVALID_HANDLE_VALUE) {
for (int i=0; i<10; i++) {
std::string name = "\\\\.\\pipe\\discord-ipc-" + std::to_string(i);
hPipe = CreateFileA(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (hPipe != INVALID_HANDLE_VALUE) {
Wh_Log(L"[VLC-RPC] Connected to Discord IPC (pipe %d).", i);
break;
}
}
if (hPipe != INVALID_HANDLE_VALUE) {
std::string hs = "{\"v\":1,\"client_id\":\"" + myClientId + "\"}";
int op=0; int l=(int)hs.length(); DWORD w; WriteFile(hPipe,&op,4,&w,NULL); WriteFile(hPipe,&l,4,&w,NULL); WriteFile(hPipe,hs.c_str(),l,&w,NULL);
isConnected = true;
} else {
static bool loggedIpcFail2 = false;
if (!loggedIpcFail2) {
Wh_Log(L"[VLC-RPC] Could not connect to Discord IPC. Is Discord running?");
loggedIpcFail2 = true;
}
}
}
if (isConnected) {
std::string state = isPlaying ? "Playing" : "Paused";
if (query.empty()) query = "VLC Media Player";
std::string btnUrl = GenerateButtonUrl(query, myProvider, myCustomUrl);
std::string js = "{\"cmd\":\"SET_ACTIVITY\",\"args\":{\"pid\":" + NumToStr(GetCurrentProcessId()) + ",\"activity\":{";
js += "\"details\":\"" + SanitizeString(top) + "\",";
if (bot.empty()) {
js += "\"state\":\"" + state + "\",";
} else {
js += "\"state\":\"" + SanitizeString(bot) + SEP + state + "\",";
}
js += "\"type\":" + NumToStr(activityType) + ",";
js += "\"name\":\"" + SanitizeString(activityName) + "\",";
js += "\"assets\":{\"large_image\":\"" + displayImage + "\",\"large_text\":\"" + SanitizeString(largeText) + "\"";
if (!bMinimalMode) {
js += ",\"small_image\":\"" + (isPlaying ? assetPlay : assetPause) + "\",\"small_text\":\"" + state + "\"";
}
js += "}";
if (isPlaying && anchorEnd > 0) {
js += ",\"timestamps\":{\"start\":" + NumToStr(anchorStart) + ",\"end\":" + NumToStr(anchorEnd) + "}";
}
js += ",\"buttons\":[{\"label\":\"" + myBtnLabel + "\",\"url\":\"" + btnUrl + "\"}]";
js += "}},\"nonce\":\"1\"}";
int op=1; int l=(int)js.length(); DWORD w;
bool s1 = WriteFile(hPipe,&op,4,&w,NULL);
bool s2 = WriteFile(hPipe,&l,4,&w,NULL);
bool s3 = WriteFile(hPipe,js.c_str(),l,&w,NULL);
if (!s1 || !s2 || !s3) { CloseHandle(hPipe); hPipe = INVALID_HANDLE_VALUE; isConnected = false; }
}
if (bShowNotifications && isPlaying && !toastFired && toastTimer >= 3) {
if (!top.empty()) {
ShowSystemToast(StrToWStr(top), StrToWStr(bot), displayImage);
}
toastFired = true;
}
lastTop = top; lastBot = bot; lastPlaying = isPlaying; lastActivityType = activityType;
lastDisplayImage = displayImage;
heartbeat = 0; lastState = stateStr;
} else {
heartbeat++;
}
}
}
}
WinHttpCloseHandle(hRequest); hRequest = NULL;
}
if (!requestSuccess) {
if (!loggedFailure) {
Wh_Log(L"[VLC-RPC] Could not reach VLC. Trying fallback ports...");
loggedFailure = true;
}
if (hConnect) { WinHttpCloseHandle(hConnect); hConnect = NULL; }
currentPortIndex = (currentPortIndex + 1) % candidatePorts.size();
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
} else {
loggedFailure = false;
for(int k=0; k<10; k++) {
if (g_stopThread.load()) break;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
}
if (isConnected && hPipe != INVALID_HANDLE_VALUE) CloseHandle(hPipe);
if (hConnect) WinHttpCloseHandle(hConnect);
if (hSession) WinHttpCloseHandle(hSession);
}
BOOL Wh_ModInit() {
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
Gdiplus::GdiplusStartup(&g_gdiplusToken, &gdiplusStartupInput, NULL);
g_stopThread = false;
g_workerThread = std::thread(Worker);
return TRUE;
}
void Wh_ModUninit() {
g_stopThread = true;
if (g_workerThread.joinable()) g_workerThread.join();
Gdiplus::GdiplusShutdown(g_gdiplusToken);
}
BOOL Wh_ModSettingsChanged(BOOL* bReload) {
*bReload = TRUE;
return TRUE;
}Track every improvement, optimization, and bug fix as we evolve the native VLC experience.
wh_log() entries to make debugging and tracking media simpler.Special thanks to @josephct for suggesting the music activity feature and for identifying the VLC port issue that could stop Rich Presence from working.
What's New:
New Features
Enhanced Customization
What's New:
Fixes & Performance Improvements
0x0.stuguu.se, which is more reliable and automatically deletes images after Visual Enhancements
Improvements & Fixes
End of recent stream
Help us expand the VLC Discord RPC collection. We're looking for clean, creative icon sets that fit the native aesthetic.
Choose your preferred way to share your icon pack with the maintainers.
Find answers to technical queries, safety concerns, and setup troubleshooting for the VLC Discord RPC ecosystem.
Still have questions? Create an issue on GitHub.