-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
374 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,78 @@ | ||
# Aegisub-DiscordRPC | ||
Outputs Aegisub editing to Discord Rich Presence | ||
# Aegisub DiscordRPC | ||
A Lua plugin (macro) for Aegisub to output currently editing | ||
subtitle information to Discord Rich Presence | ||
|
||
## Installation | ||
- Place the binary file `discord-rpc.dll` in your Aegisub installation folder. | ||
- If you use the discouraged 64-bit Aegisub then please take the | ||
provided `discord-rpc-64.dll` and rename it as `discord-rpc.dll` | ||
before placing it in your Aegisub installation folder. | ||
(I don't guarantee if it would work in 64-bit Aegisub because I | ||
don't use them and I can't test it. Plus, the official website | ||
also discouraged the use of 64-bit release due to lack of support | ||
for most of its video tools and renderer) | ||
- Place the script `discord-rpc.lua` into the `automation\autoload` folder. | ||
|
||
### Example | ||
If you installed Aegisub in `C:\Program Files (x86)\Aegisub` then: | ||
- Place `discord-rpc.dll` in `C:\Program Files (x86)\Aegisub`. | ||
- Place `discord-rpc.lua` in `C:\Program Files (x86)\Aegisub\automation\autoload`. | ||
|
||
## Usage | ||
When installed correctly, it will first start the rich presense | ||
with `Idle` as the detail and `No video file loaded yet` as the state | ||
whenever you launched Aegisub. | ||
|
||
![First launch](./img/first-launch.png "First launch") | ||
|
||
Then, load your subtitle and video, and click on `Update Discord RPC` | ||
from the `Automation` menu to let Aegisub update the Rich Presence | ||
as `Editing subtitle` as the detail and the video file name as | ||
the state. | ||
|
||
![Click on the menu](./img/click-menu.png "Click on the menu") | ||
![Details are updated](./img/detail-updated.png "Details are updated") | ||
|
||
### Warning | ||
Please make sure that you have done either any of below before | ||
clicking `Update Discord RPC` menu: | ||
- You have already loaded the video for the subtitle you're editing. | ||
- You have already loaded subtitle file containing video path. | ||
|
||
This is because of limitation in Aegisub, it doesn't expose the | ||
subtitle file name nor the window title to its Lua environment. | ||
It does, however, expose the subtitle property metadata to the | ||
Lua environment, and video path is one of the metadata in it. | ||
|
||
I'm accessing that video path from the subtitle metadata for | ||
display in Rich Presence, and it would not work if you did not | ||
load any video for the subtitle file ever as the video path will | ||
not be there in the subtitle metadata. | ||
|
||
I initially planned to automatically load the video path when | ||
Aegisub is launched, but it seems that the Lua script is loaded | ||
*before* the subtitle metadata itself, which is why I had to | ||
implement the "click on menu to update Discord RPC" method. | ||
|
||
## References | ||
### Script | ||
This script uses some code from pfirsich's `lua-discordRPC`. | ||
Visit [their repository](https://github.com/pfirsich/lua-discordRPC "pfirsich/lua-discordRPC on GitHub") | ||
to learn more, especially if you want to use Discord Rich Presence in Lua. | ||
|
||
Their script is licensed under MIT. | ||
|
||
### Binary | ||
The binaries included in this repository came from Discord RPC | ||
official releases. You can also download the binaries directly from | ||
their [official repository](https://github.com/discordapp/discord-rpc/releases/latest "Latest Discord RPC release"). | ||
|
||
Make sure to take the dynamic version of binary: | ||
- Take `discord-rpc/win32-dynamic/bin/discord-rpc.dll` for 32-bit Aegisub | ||
- Take `discord-rpc/win64-dynamic/bin/discord-rpc.dll` for 64-bit Aegisub | ||
|
||
Their binaries and source code are licensed under MIT. | ||
|
||
## License | ||
This script and the included binaries are released under | ||
[The MIT License](./LICENSE "Read full license text"). |
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,296 @@ | ||
-- Discord RPC | ||
-- Outputs current editing session to Discord Rich Presence | ||
-- | ||
-- This file is written by muhdnurhidayat | ||
-- Latest release and details at https://github.com/MuhdNurHidayat/Aegisub-DiscordRPC | ||
-- This file is licensed under MIT | ||
-- | ||
-- This file uses some code from https://github.com/pfirsich/lua-discordRPC | ||
-- The codes from pfirsich/lua-discordRPC is licensed under MIT | ||
-- | ||
|
||
local ffi = require "ffi" | ||
local discordRPClib = ffi.load("discord-rpc") | ||
local appId = "592657785368477728" | ||
|
||
script_name = "Discord RPC" | ||
script_description = "Outputs Aegisub editing to Discord Rich Presence" | ||
script_author = "muhdnurhidayat" | ||
script_version = "1" | ||
|
||
ffi.cdef[[ | ||
typedef struct DiscordRichPresence { | ||
const char* state; /* max 128 bytes */ | ||
const char* details; /* max 128 bytes */ | ||
int64_t startTimestamp; | ||
int64_t endTimestamp; | ||
const char* largeImageKey; /* max 32 bytes */ | ||
const char* largeImageText; /* max 128 bytes */ | ||
const char* smallImageKey; /* max 32 bytes */ | ||
const char* smallImageText; /* max 128 bytes */ | ||
const char* partyId; /* max 128 bytes */ | ||
int partySize; | ||
int partyMax; | ||
const char* matchSecret; /* max 128 bytes */ | ||
const char* joinSecret; /* max 128 bytes */ | ||
const char* spectateSecret; /* max 128 bytes */ | ||
int8_t instance; | ||
} DiscordRichPresence; | ||
|
||
typedef struct DiscordUser { | ||
const char* userId; | ||
const char* username; | ||
const char* discriminator; | ||
const char* avatar; | ||
} DiscordUser; | ||
|
||
typedef void (*readyPtr)(const DiscordUser* request); | ||
typedef void (*disconnectedPtr)(int errorCode, const char* message); | ||
typedef void (*erroredPtr)(int errorCode, const char* message); | ||
typedef void (*joinGamePtr)(const char* joinSecret); | ||
typedef void (*spectateGamePtr)(const char* spectateSecret); | ||
typedef void (*joinRequestPtr)(const DiscordUser* request); | ||
|
||
typedef struct DiscordEventHandlers { | ||
readyPtr ready; | ||
disconnectedPtr disconnected; | ||
erroredPtr errored; | ||
joinGamePtr joinGame; | ||
spectateGamePtr spectateGame; | ||
joinRequestPtr joinRequest; | ||
} DiscordEventHandlers; | ||
|
||
void Discord_Initialize(const char* applicationId, | ||
DiscordEventHandlers* handlers, | ||
int autoRegister, | ||
const char* optionalSteamId); | ||
|
||
void Discord_Shutdown(void); | ||
|
||
void Discord_RunCallbacks(void); | ||
|
||
void Discord_UpdatePresence(const DiscordRichPresence* presence); | ||
|
||
void Discord_ClearPresence(void); | ||
|
||
void Discord_Respond(const char* userid, int reply); | ||
|
||
void Discord_UpdateHandlers(DiscordEventHandlers* handlers); | ||
]] | ||
|
||
local discordRPC = {} -- module table | ||
|
||
-- proxy to detect garbage collection of the module | ||
discordRPC.gcDummy = newproxy(true) | ||
|
||
local function unpackDiscordUser(request) | ||
return ffi.string(request.userId), ffi.string(request.username), | ||
ffi.string(request.discriminator), ffi.string(request.avatar) | ||
end | ||
|
||
-- callback proxies | ||
-- note: callbacks are not JIT compiled (= SLOW), try to avoid doing performance critical tasks in them | ||
-- luajit.org/ext_ffi_semantics.html | ||
local ready_proxy = ffi.cast("readyPtr", function(request) | ||
if discordRPC.ready then | ||
discordRPC.ready(unpackDiscordUser(request)) | ||
end | ||
end) | ||
|
||
local disconnected_proxy = ffi.cast("disconnectedPtr", function(errorCode, message) | ||
if discordRPC.disconnected then | ||
discordRPC.disconnected(errorCode, ffi.string(message)) | ||
end | ||
end) | ||
|
||
local errored_proxy = ffi.cast("erroredPtr", function(errorCode, message) | ||
if discordRPC.errored then | ||
discordRPC.errored(errorCode, ffi.string(message)) | ||
end | ||
end) | ||
|
||
-- helpers | ||
function checkArg(arg, argType, argName, func, maybeNil) | ||
assert(type(arg) == argType or (maybeNil and arg == nil), | ||
string.format("Argument \"%s\" to function \"%s\" has to be of type \"%s\"", | ||
argName, func, argType)) | ||
end | ||
|
||
function checkStrArg(arg, maxLen, argName, func, maybeNil) | ||
if maxLen then | ||
assert(type(arg) == "string" and arg:len() <= maxLen or (maybeNil and arg == nil), | ||
string.format("Argument \"%s\" of function \"%s\" has to be of type string with maximum length %d", | ||
argName, func, maxLen)) | ||
else | ||
checkArg(arg, "string", argName, func, true) | ||
end | ||
end | ||
|
||
function checkIntArg(arg, maxBits, argName, func, maybeNil) | ||
maxBits = math.min(maxBits or 32, 52) -- lua number (double) can only store integers < 2^53 | ||
local maxVal = 2^(maxBits-1) -- assuming signed integers, which, for now, are the only ones in use | ||
assert(type(arg) == "number" and math.floor(arg) == arg | ||
and arg < maxVal and arg >= -maxVal | ||
or (maybeNil and arg == nil), | ||
string.format("Argument \"%s\" of function \"%s\" has to be a whole number <= %d", | ||
argName, func, maxVal)) | ||
end | ||
|
||
-- function wrappers | ||
function discordRPC.initialize(applicationId, autoRegister, optionalSteamId) | ||
local func = "discordRPC.Initialize" | ||
checkStrArg(applicationId, nil, "applicationId", func) | ||
checkArg(autoRegister, "boolean", "autoRegister", func) | ||
if optionalSteamId ~= nil then | ||
checkStrArg(optionalSteamId, nil, "optionalSteamId", func) | ||
end | ||
|
||
local eventHandlers = ffi.new("struct DiscordEventHandlers") | ||
eventHandlers.ready = ready_proxy | ||
eventHandlers.disconnected = disconnected_proxy | ||
eventHandlers.errored = errored_proxy | ||
eventHandlers.joinGame = joinGame_proxy | ||
eventHandlers.spectateGame = spectateGame_proxy | ||
eventHandlers.joinRequest = joinRequest_proxy | ||
|
||
discordRPClib.Discord_Initialize(applicationId, eventHandlers, | ||
autoRegister and 1 or 0, optionalSteamId) | ||
end | ||
|
||
function discordRPC.shutdown() | ||
discordRPClib.Discord_Shutdown() | ||
end | ||
|
||
function discordRPC.runCallbacks() | ||
-- http://luajit.org/ext_ffi_semantics.html#callback : | ||
-- One thing that's not allowed, is to let an FFI call into a C function (runCallbacks) | ||
-- get JIT-compiled, which in turn calls a callback, calling into Lua again (i.e. discordRPC.ready). | ||
-- Usually this attempt is caught by the interpreter first and the C function | ||
-- is blacklisted for compilation. | ||
-- solution: | ||
-- Then you'll need to manually turn off JIT-compilation with jit.off() for | ||
-- the surrounding Lua function that invokes such a message polling function. | ||
jit.off() | ||
discordRPClib.Discord_RunCallbacks() | ||
jit.on() | ||
end | ||
|
||
function discordRPC.updatePresence(presence) | ||
local func = "discordRPC.updatePresence" | ||
checkArg(presence, "table", "presence", func) | ||
|
||
-- -1 for string length because of 0-termination | ||
checkStrArg(presence.state, 127, "presence.state", func, true) | ||
checkStrArg(presence.details, 127, "presence.details", func, true) | ||
|
||
checkIntArg(presence.startTimestamp, 64, "presence.startTimestamp", func, true) | ||
checkIntArg(presence.endTimestamp, 64, "presence.endTimestamp", func, true) | ||
|
||
checkStrArg(presence.largeImageKey, 31, "presence.largeImageKey", func, true) | ||
checkStrArg(presence.largeImageText, 127, "presence.largeImageText", func, true) | ||
checkStrArg(presence.smallImageKey, 31, "presence.smallImageKey", func, true) | ||
checkStrArg(presence.smallImageText, 127, "presence.smallImageText", func, true) | ||
checkStrArg(presence.partyId, 127, "presence.partyId", func, true) | ||
|
||
checkIntArg(presence.partySize, 32, "presence.partySize", func, true) | ||
checkIntArg(presence.partyMax, 32, "presence.partyMax", func, true) | ||
|
||
checkStrArg(presence.matchSecret, 127, "presence.matchSecret", func, true) | ||
checkStrArg(presence.joinSecret, 127, "presence.joinSecret", func, true) | ||
checkStrArg(presence.spectateSecret, 127, "presence.spectateSecret", func, true) | ||
|
||
checkIntArg(presence.instance, 8, "presence.instance", func, true) | ||
|
||
local cpresence = ffi.new("struct DiscordRichPresence") | ||
cpresence.state = presence.state | ||
cpresence.details = presence.details | ||
cpresence.startTimestamp = presence.startTimestamp or 0 | ||
cpresence.endTimestamp = presence.endTimestamp or 0 | ||
cpresence.largeImageKey = presence.largeImageKey | ||
cpresence.largeImageText = presence.largeImageText | ||
cpresence.smallImageKey = presence.smallImageKey | ||
cpresence.smallImageText = presence.smallImageText | ||
cpresence.partyId = presence.partyId | ||
cpresence.partySize = presence.partySize or 0 | ||
cpresence.partyMax = presence.partyMax or 0 | ||
cpresence.matchSecret = presence.matchSecret | ||
cpresence.joinSecret = presence.joinSecret | ||
cpresence.spectateSecret = presence.spectateSecret | ||
cpresence.instance = presence.instance or 0 | ||
|
||
discordRPClib.Discord_UpdatePresence(cpresence) | ||
end | ||
|
||
function discordRPC.clearPresence() | ||
discordRPClib.Discord_ClearPresence() | ||
end | ||
|
||
local replyMap = { | ||
no = 0, | ||
yes = 1, | ||
ignore = 2 | ||
} | ||
|
||
-- maybe let reply take ints too (0, 1, 2) and add constants to the module | ||
function discordRPC.respond(userId, reply) | ||
checkStrArg(userId, nil, "userId", "discordRPC.respond") | ||
assert(replyMap[reply], "Argument 'reply' to discordRPC.respond has to be one of \"yes\", \"no\" or \"ignore\"") | ||
discordRPClib.Discord_Respond(userId, replyMap[reply]) | ||
end | ||
|
||
-- garbage collection callback | ||
getmetatable(discordRPC.gcDummy).__gc = function() | ||
discordRPC.shutdown() | ||
ready_proxy:free() | ||
disconnected_proxy:free() | ||
errored_proxy:free() | ||
end | ||
|
||
function discordRPC.ready(userId, username, discriminator, avatar) | ||
print("[discordrpc] Discord: ready (" .. userId .. ", " .. username .. ", " .. discriminator ", " .. avatar .. ")") | ||
end | ||
|
||
function discordRPC.disconnected(errorCode, message) | ||
print("[discordrpc] Discord: disconnected (" .. errorCode .. ": " .. message .. ")") | ||
end | ||
|
||
function discordRPC.errored(errorCode, message) | ||
print("[discordrpc] Discord: error (" .. errorCode .. ": " .. message .. ")") | ||
end | ||
|
||
discordRPC.initialize(appId, true) | ||
local now = os.time(os.date('*t')) | ||
presence = { | ||
state = "No video file loaded yet", | ||
details = "Idle", | ||
startTimestamp = now, | ||
largeImageKey = "aegisub", | ||
smallImageKey = "", | ||
} | ||
discordRPC.updatePresence(presence) | ||
|
||
function update_rpc() | ||
if (aegisub.project_properties() ~= nil) then | ||
local videoname = aegisub.project_properties().video_file; | ||
if (videoname ~= " ") then | ||
videoname = videoname:match("[^\\]*$") | ||
if(string.len(videoname) > 117) then | ||
videoname = string.sub(videoname, 1, 117) .. "…" | ||
end | ||
presence = { | ||
state = "Video: " .. videoname, | ||
details = "Editing subtitle", | ||
startTimestamp = now, | ||
largeImageKey = "aegisub", | ||
smallImageKey = "", | ||
} | ||
discordRPC.updatePresence(presence) | ||
else | ||
aegisub.debug.out("Please ensure your subtitle file has video file path defined before updating the RPC") | ||
end | ||
else | ||
aegisub.debug.out("Please ensure your subtitle file has video file path defined before updating the RPC") | ||
end | ||
end | ||
|
||
aegisub.register_macro("Update Discord RPC", "Update Discord Rich Presence", update_rpc) |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.