view src/plugin.c @ 11:e6a594f16403

*: huge refactor the config file has changed drastically, moving to an ini file from that custom format; i *would* have used the win32 functions for those, but they were barely functional, so I decided on using ini.h which is lightweight enough. additionally, I've added Deezer support so album art will be displayed! unfortunately though winhttp is a pain in the ass so if I send a request with any form of unicode chars in it it just returns a "bad request" error. I've tried debugging this but I could never really come up with anything: my hypothesis is that deezer expects their characters in percent-encoded UTF-8, but winhttp is sending them in some other encoding. the config dialog was moved out of config.c (overdue) and many more options are given in the config as well. main.c has been renamed to plugin.c to better differentiate it from... everything else.
author Paper <paper@paper.us.eu.org>
date Thu, 14 Mar 2024 20:25:37 -0400
parents
children
line wrap: on
line source

#include "plugin.h"
#include "deezer.h"
#include "timer.h"
#include "config.h"
#include "resource.h"
#include "utils.h"
#include "dialog/dlg_config.h"

/* ugh. */
#ifndef _MSC_VER
#define _MSC_VER 1201
#define WGSDK_UGLY_MSC_HACK 1
#endif

#include <Winamp/wa_ipc.h>

#ifdef WGSDK_UGLY_MSC_HACK
#undef _MSC_VER
#endif

#include "discord_game_sdk.h"

#include <assert.h>

#define CLIENT_ID (969367220599803955)
#define GPPHDR_VER (0x10)
#define METADATA_ITEM_SIZE (256)

int  init();
void conf();
void quit();

struct winamp_gpp g_plugin = {
	GPPHDR_VER,			// version of the plugin, DO NOT change
	"Discord GameSDK",	// name of the plugin
	init,				// function pointer, executed on init event
	conf,				// function pointer, executed on config event
	quit,				// function pointer, executed on quit event
	NULL,				// Winamp main window HWND, loaded by Winamp
	NULL				// HINSTANCE to this DLL, loaded by Winamp
};

/* Discord stuff */

struct application {
	struct IDiscordCore* core;
	struct IDiscordActivityManager* activities;
};

static struct application app = {0};

/* Now we get to our built-in stuff */

struct timer timer_callbacks = {0};

WNDPROC _winamp_proc = NULL;

/* ------------------------------------ */

void DISCORD_CALLBACK update_activity_callback(void* data, enum EDiscordResult result) {
	/* no-op */
}

/* a bunch of spaghetti... */
void report_current_song_status(int playback_state) {
	/* struct to be filled by us */
	struct DiscordActivity activity = {0};

	assert(playback_state != 0);

	activity.application_id = CLIENT_ID;
	strcpy(activity.name, "Winamp");

	if (config.show_elapsed_time && playback_state == 1) {
		/* don't even bother trying to get the elapsed time. */
		activity.timestamps.start = get_system_time_in_milliseconds();

		/* this is how we can get the end timestamp if we want to:
		 *
		 * LRESULT track_length = SendMessage(g_plugin.hwndParent, WM_WA_IPC, 2, IPC_GETOUTPUTTIME);
		 * activity.timestamps.end = get_system_time_in_milliseconds() + track_length;
		*/
	}

	LPCWSTR filename = (LPCWSTR)SendMessageW(g_plugin.hwndParent, WM_WA_IPC, 0, IPC_GET_PLAYING_FILENAME);

	WCHAR title[METADATA_ITEM_SIZE + 1] = {L'\0'};
	extendedFileInfoStructW file_info = {filename, L"title", title, METADATA_ITEM_SIZE};

	/* please excuse the spaghetti here, I swear it wasn't this bad before */
	int have_metadata = SendMessageW(g_plugin.hwndParent, WM_WA_IPC, (WPARAM)&file_info, IPC_GET_EXTENDED_FILE_INFOW);
	if (have_metadata) {
		WCHAR album[METADATA_ITEM_SIZE + 1] = {L'\0'};
		WCHAR artist[METADATA_ITEM_SIZE + 1] = {L'\0'};

		/* grab artist info */
		file_info.metadata = L"artist";
		file_info.ret = artist;
		SendMessageW(g_plugin.hwndParent, WM_WA_IPC, (WPARAM)&file_info, IPC_GET_EXTENDED_FILE_INFOW);
		if (artist[0] == '\0') {
			/* fallback to album artist */
			file_info.metadata = L"album artist";
			SendMessageW(g_plugin.hwndParent, WM_WA_IPC, (WPARAM)&file_info, IPC_GET_EXTENDED_FILE_INFOW);
		}

		/* grab album info */
		file_info.metadata = L"album";
		file_info.ret = album;
		SendMessageW(g_plugin.hwndParent, WM_WA_IPC, (WPARAM)&file_info, IPC_GET_EXTENDED_FILE_INFOW);

		/* get thumbnail URL */
		if (config.display_song_info && config.display_album_art) {
			char* image_url = deezer_get_thumbnail(artist, album);
			strncpy(activity.assets.large_image, image_url ? image_url : "winamp-logo", ARRAYSIZE(activity.assets.large_image) - 1);
			free(image_url); /* freeing NULL is a no-op */
		}

		/* do NOT use something like ARRAYSIZE here, we need the size of the array *in bytes* */
		do {
			size_t activity_details_offset = 0;

			if (artist[0] && config.display_song_info && config.display_artist_name) {
				size_t off = append_wstr_to_utf8(artist, activity.details, activity_details_offset, sizeof(activity.details));
				if (!off) break;
				else activity_details_offset += off;
			}

			if (artist[0] && album[0] && config.display_song_info && config.display_artist_name && config.display_album_name) {
				LPCWSTR delimiter = L" - ";
				size_t off = append_wstr_to_utf8(delimiter, activity.details, activity_details_offset, sizeof(activity.details));
				if (!off) break;
				else activity_details_offset += off;
			}

			if (album[0] && config.display_song_info && config.display_album_name) {
				size_t off = append_wstr_to_utf8(album, activity.details, activity_details_offset, sizeof(activity.details));
				if (!off) break;
				else activity_details_offset += off;
			}
		} while (0);
	} else {
		/* fallback to basic info */
		wchar_t* winamp_title = (wchar_t*)SendMessageW(g_plugin.hwndParent, WM_WA_IPC, 0, IPC_GET_PLAYING_TITLE);
		wcsncpy(title, winamp_title, METADATA_ITEM_SIZE);
	}

	size_t activity_state_offset = 0;
	do {
		size_t off = 0;

		if (title[0] && config.display_song_info && config.display_title) {
			off = append_wstr_to_utf8(title, activity.state, activity_state_offset, sizeof(activity.state));
			if (!off) break;
			else activity_state_offset += off;

			LPCWSTR title_delimiter = L" ";
			off = append_wstr_to_utf8(title_delimiter, activity.state, activity_state_offset, sizeof(activity.state));
			if (!off) break;
			else activity_state_offset += off;
		}

		LPCWSTR title_playing = (playback_state == 1) ? L"(Playing)" : L"(Stopped)";
		off = append_wstr_to_utf8(title_playing, activity.state, activity_state_offset, sizeof(activity.state));
		if (!off) break;
		else activity_state_offset += off;
	} while (0);

	app.activities->update_activity(app.activities, &activity, &app, update_activity_callback);
}

void report_idle_status(void) {
	struct DiscordActivity activity = {0};

	activity.application_id = CLIENT_ID;
	strcpy(activity.name, "Winamp");
	strcpy(activity.state, "(Idle)");
	strcpy(activity.assets.large_image, "winamp-logo");

	app.activities->update_activity(app.activities, &activity, &app, update_activity_callback);
}

void update_rich_presence_details(void) {
	LONG is_playing = SendMessageW(g_plugin.hwndParent, WM_WA_IPC, 0, IPC_ISPLAYING);

	switch (is_playing) {
		case 0:
			report_idle_status();
			break;
		case 1:
		case 3:
			report_current_song_status(is_playing);
			break;
		default:
			break;
	}
}

void CALLBACK TimerProc(HWND, UINT, UINT_PTR, DWORD) {
	app.core->run_callbacks(app.core);
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) {
	if (message == WM_WA_IPC && lParam == IPC_CB_MISC && wParam == IPC_CB_MISC_STATUS)
		update_rich_presence_details();

	return CallWindowProc(_winamp_proc, hwnd, message, wParam, lParam);
}

int init() {
	struct DiscordCreateParams params = {0};

	DiscordCreateParamsSetDefault(&params);
	params.client_id = CLIENT_ID;
	params.flags = DiscordCreateFlags_Default;
	params.event_data = &app;

	if (DiscordCreate(DISCORD_VERSION, &params, &app.core) != DiscordResult_Ok)
		return -1;

	/* don't do this if we don't have discord */
	_winamp_proc = (IsWindowUnicode(g_plugin.hwndParent))
		? (WNDPROC)SetWindowLongPtrW(g_plugin.hwndParent, GWLP_WNDPROC, (LONG_PTR)WndProc)
		: (WNDPROC)SetWindowLongPtrA(g_plugin.hwndParent, GWLP_WNDPROC, (LONG_PTR)WndProc);

	app.activities = app.core->get_activity_manager(app.core);

	timer_init(&timer_callbacks, 16, TimerProc);
	timer_set(&timer_callbacks);

	cfg_load(&config);

	update_rich_presence_details();

	return 0;
}

void quit() {
	assert(!cfg_save(&config));
	app.activities->clear_activity(app.activities, &app, update_activity_callback);
	timer_stop(&timer_callbacks);
	close_open_http_handles();
}

void conf() {
	DialogBoxW(g_plugin.hDllInstance, (LPWSTR)DIALOG_CONFIG, g_plugin.hwndParent, (DLGPROC)cfg_win_proc);
}

__declspec(dllexport) struct winamp_gpp* winampGetGeneralPurposePlugin() {
	return &g_plugin;
}