view src/plugin.c @ 12:dd427b7cc459 default tip

json: replace with nxjson library more lightweight, reduces the binary size by about 40 kb
author Paper <paper@paper.us.eu.org>
date Fri, 15 Mar 2024 20:46:18 -0400
parents e6a594f16403
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;
}