// ==WindhawkMod==
// @id vlc-discord-rpc
// @name VLC Discord Rich Presence
// @description Shows your playing status, quality tags (4K/HDR), and interactive buttons on Discord.
// @version 1.1.3
// @author ciizerr
// @github https://github.com/ciizerr
// @homepage https://vlc-rpc.vercel.app/
// @include vlc.exe
// @compilerOptions -lwinhttp
// @architecture x86
// @architecture x86-64
// ==/WindhawkMod==
// ==WindhawkModReadme==
/*
# VLC Discord Rich Presence
Seamlessly integrates VLC Media Player with Discord to display playback status, media metadata, resolution tags, and **Album Artwork**.
## Features
* **Smart Cover Art Engine:** Automatically uploads local album art to (~~`0x0.st`~~) `Uguu.se`. If local art is missing, it intelligently scrapes high-res posters and album covers directly from the web(accuracy depends on how well filename matches the title).
* **Metadata Cleaner:** Intelligently strips piracy site URLs, promotional phrases, bracketed tags, and scene release technical info to guarantee perfect Discord display titles.
* **Custom Junk Filter:** Define your own list of annoying tags or site names to automatically remove from media titles.
* **Smart Activity Status:** Dynamically switches between "Listening to **Song**", "Watching **Movie**", or "Playing **Video**" based on the file type.
* **Clean Metadata:** * **Music:** Displays Song Title, Artist, and Album.
* **Video:** Displays Title, Season/Episode, Chapter, and Audio Language.
* **Quality Tags:** Displays resolution and format tags (4K, HDR, 1080p, 10-bit) based on the media file.
* **Interactive Buttons:** Adds a "Search This" button to your status, redirecting to Google, IMDb, or YouTube.
* **Visual Themes & Layouts:** Includes options for Default/Dark Mode icon sets, and a Minimal toggle to hide small badges.
## Icon Themes
Users can customize the appearance of the Rich Presence icons via the Mod Settings.
* **Default:** The standard orange VLC cone.

* **Dark:** A dark-mode variant for low-light aesthetics.

**Submissions:** We are accepting community designs for new icon themes. If you have created a set (vlc, play, pause, stop), please contact `ciizerr` on Discord.
## Setup Instructions (First Run Only)
For this mod to retrieve data from VLC, the Web Interface must be enabled.
1. Open VLC Media Player.
2. Go to **Tools** > **Preferences** (or press `Ctrl+P`).
3. In the bottom-left corner, under *Show settings*, select **All**.
4. Navigate to **Interface** > **Main interfaces**.
5. On the right panel, check the box for **Web**.

6. In the left sidebar, expand *Main interfaces* and click on **Lua**.
7. Under *Lua HTTP*, set the **Password** to `1234` and **Port** to `8080`.

8. Click **Save** and restart VLC.
## Configuration
**Show Cover Art:** Toggle to enable/disable the fetching and uploading of cover art. If disabled, the mod will use the standard VLC icon.
**Custom Junk Filter:** Define your own list of annoying tags or site names to automatically remove from media titles.
**Strict Local Filters (Transparency):** To keep the metadata cleaner accurate against new piracy tags, the mod fetches a tiny text file [`filters.txt`](https://raw.githubusercontent.com/ciizerr/vlc-discord-rpc-archive/main/assets/filters.txt) from the GitHub repository if the file is older than 6 hours. If you prefer **zero** external network requests, enable `Strict Local Filters Only`. This will restrict the metadata cleaner to only use the built-in hardcoded dictionary + your custom words.
**Search Provider:** You can change the destination of the search button (Google, Bing, IMDb) in the mod settings.
**Custom Client ID:** Power users who wish to upload their own assets can provide a custom Application ID in the settings.
## Feedback & Support
For bug reports, feature suggestions, or general feedback, please reach out via:
* **Discord:** `ciizerr`
* **GitHub:** [vlc-discord-rpc-archive](https://github.com/ciizerr/vlc-discord-rpc-archive) (contains cross-platform resources of vlc-discord-rpc)
*/
// ==/WindhawkModReadme==
// ==WindhawkModSettings==
/*
- ClientId: "1465711556418474148"
$name: Discord Client ID
$description: "The Application ID from the Discord Developer Portal. Leave default to use the official one."
- ShowCoverArt: true
$name: Show Cover Art
$description: "If enabled, attempts to upload local art or fetch online posters via smart web search. Disable to use the standard VLC icon."
- ShowQualityTags: true
$name: Show Quality Tags
$description: "If enabled, displays resolution and format tags (4K, HDR, 1080p). Disable for a cleaner status layout."
- EnableMetadataCleaner: true
$name: Clean Media Titles
$description: "Automatically removes common scene tags (e.g., WEB-DL, 1080p) and URLs from filenames so they look clean on Discord."
- StrictLocalMode: true
$name: Strict Local Filters Only
$description: "If enabled, stops downloading community filter updates from GitHub and only relies on the built-in hardcoded filters and your custom words. See README."
- CustomJunkWords: ""
$name: Additional Words to Remove (Optional)
$description: "Add your own custom words to remove, separated by commas (e.g., toonworld4all.com, custom-tag). Note: 'Clean Media Titles' must be enabled above for this to work."
- MinimalMode: false
$name: Minimal Mode
$description: "Hide the small play/pause/stop badges in the corner to let the cover art shine."
- Theme: ""
$name: Icon Theme
$description: "Choose the visual style of the primary icons on your Discord status."
$options:
- "": Default (vlc_icon)
- "dark_": Dark Mode (dark_vlc_icon)
- Provider: Google
$name: Search Provider
$options:
- Google: Google
- Bing: Bing
- IMDb: IMDb
- YouTube: YouTube
- Custom: Custom URL
- CustomUrl: ""
$name: Custom URL
$description: "For other sites (Yahoo, MyAnimeList), enter their search URL here. Example: https://myanimelist.net/search/all?q="
- ButtonLabel: "Search This"
$name: Button Label
$description: "The text displayed on the Discord button (Max 30 chars)."
*/
// ==/WindhawkModSettings==
#include <windows.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
// =============================================================
std::atomic<bool> g_stopThread{false};
std::thread g_workerThread;
const std::wstring VLC_PASS_BASE64 = L"OjEyMzQ=";
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 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;
}
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/search?q=" + term);
std::string html = FetchHttps(host, path);
std::string searchToken = "id=OIP.";
size_t start = html.find(searchToken);
if (start != std::string::npos) {
start += 3;
size_t endQuote = html.find("\"", start);
size_t endAmp = html.find("&", start);
size_t end = std::min(endQuote, endAmp);
if (end != std::string::npos) {
std::string imageId = html.substr(start, end - start);
finalUrl = "https://tse1.mm.bing.net/th?id=" + imageId;
}
}
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;
}
// 🔥 V1.1.4: ASYNC UGUU UPLOAD ENGINE 🔥
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; }
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;
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;
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. 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 bEnableMetadataCleaner = Wh_GetIntSetting(L"EnableMetadataCleaner");
// 🔥 NEW MINIMAL TOGGLE SETTING 🔥
bool bMinimalMode = Wh_GetIntSetting(L"MinimalMode");
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);
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 = "";
while (!g_stopThread.load()) {
if (hSession && !hConnect) hConnect = WinHttpConnect(hSession, L"127.0.0.1", 8080, 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 " + VLC_PASS_BASE64;
if (WinHttpSendRequest(hRequest, headers.c_str(), headers.length(), WINHTTP_NO_REQUEST_DATA, 0, 0, 0) &&
WinHttpReceiveResponse(hRequest, NULL)) {
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) 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;
}
}
if (isConnected) {
std::string js = "{\"cmd\":\"SET_ACTIVITY\",\"args\":{\"pid\":" + NumToStr(GetCurrentProcessId()) + ",\"activity\":{";
js += "\"details\":\"Idling\",\"state\":\"Waiting for media...\",\"type\":0,";
// Omit small image if Minimal mode is enabled
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"));
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) {
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) {
activityName = title.empty() ? filename : title;
top = activityName;
query = activityName + " " + 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 (chapter >= 0) bot += SEP + "Ch " + NumToStr(chapter + 1);
if (!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;
if (chapter >= 0) bot = "Ch " + NumToStr(chapter + 1); else bot = "Video";
if (!audio.empty()) bot += SEP + audio;
query = title;
largeText = "Watching Movie";
}
else {
activityName = filename;
top = filename;
if (bShowQualityTags && !quality.empty()) top += SEP + quality;
bot = "Video";
query = filename;
largeText = "Watching Video";
}
}
if (activityName.empty()) activityName = "VLC Media Player";
std::string displayImage = assetLarge;
if (bShowCoverArt) {
std::string targetCacheKey = "";
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) 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;
}
}
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) + "\",";
js += "\"state\":\"" + SanitizeString(bot) + " (" + state + ")\",";
js += "\"type\":" + NumToStr(activityType) + ",";
js += "\"name\":\"" + SanitizeString(activityName) + "\",";
// Omit small image if Minimal mode is enabled
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; }
}
lastTop = top; lastBot = bot; lastPlaying = isPlaying; lastActivityType = activityType;
lastDisplayImage = displayImage;
heartbeat = 0; lastState = stateStr;
} else {
heartbeat++;
}
}
}
}
WinHttpCloseHandle(hRequest); hRequest = NULL;
}
if (!requestSuccess) {
if (hConnect) { WinHttpCloseHandle(hConnect); hConnect = NULL; }
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
} else {
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() {
g_stopThread = false;
g_workerThread = std::thread(Worker);
return TRUE;
}
void Wh_ModUninit() {
g_stopThread = true;
if (g_workerThread.joinable()) g_workerThread.join();
}
BOOL Wh_ModSettingsChanged(BOOL* bReload) {
*bReload = TRUE;
return TRUE;
}