Skip to content

Commit

Permalink
feat(Emby): add support for new and old versions
Browse files Browse the repository at this point in the history
Co-Authored-By: slowlife <[email protected]>
  • Loading branch information
darkvillager2 and Slowlife01 committed Oct 13, 2024
1 parent 2d211bb commit 63705e2
Showing 1 changed file with 130 additions and 33 deletions.
163 changes: 130 additions & 33 deletions websites/E/Emby/presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
* The interfaces may have some things missing,
* I've tried to set as many properties as I could find.
*/
let version: string,
showMediaTimestamp = false;

const uploadedMediaCache = new Map<string, string>();

interface ApiClient {
enableAutomaticBitrateDetection: boolean;
Expand Down Expand Up @@ -86,7 +90,7 @@ interface ApiClient {
IsLocal: boolean;
};
_serverAddress: string;
_serverInfo: Server;
_serverInfo: Server | ServerLatest;
_serverVersion: string;
_webSocket: {
binaryType: string;
Expand Down Expand Up @@ -257,6 +261,30 @@ interface MediaInfo {
Height: number;
}

interface ServerLatest {
ManualAddress: string;
ManualAddressOnly: Boolean;

Check failure on line 266 in websites/E/Emby/presence.ts

View workflow job for this annotation

GitHub Actions / Compile and Lint

Don't use `Boolean` as a type. Use boolean instead
IsLocalServer: true;
UserId: string;
DateLastAccessed: number;
LastConnectionMode: number;
Type: string;
Name: string;
Id: string;
Users: [
{
UserId: string;
AccessToken: string;
},
{
UserId: string;
AccessToken: string;
}
];
LocalAddress: string;
RemoteAddress: string;
}

interface Server {
AccessToken: string;
DateLastAccessed: number; // timestamp
Expand Down Expand Up @@ -292,6 +320,27 @@ const // official website

let presence: Presence, ApiClient: ApiClient;

async function createImageBlob() {
const largeImageKey = presenceData.largeImageKey as string;
if (isPrivateIP(largeImageKey)) {
if (uploadedMediaCache.has(largeImageKey))
presenceData.largeImageKey = uploadedMediaCache.get(largeImageKey);
else {
await fetch(largeImageKey)
.then(res => res.blob())
.then(blob => {
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
const result = reader.result as string;
uploadedMediaCache.set(largeImageKey, result);
presenceData.largeImageKey = result;
};
});
}
}
}

/**
* handleOfficialWebsite - handle the presence while the user is in the official website
*/
Expand Down Expand Up @@ -341,7 +390,8 @@ function handleOfficialWebsite(): void {
* @return {ApiClient} ApiClient object
*/
async function getApiClient() {
return presence.getPageletiable<ApiClient>("ApiClient");
const actualData = await presence.getPageVariable("ApiClient");
return actualData["ApiClient"] as ApiClient;

Check failure on line 394 in websites/E/Emby/presence.ts

View workflow job for this annotation

GitHub Actions / Compile and Lint

["ApiClient"] is better written in dot notation
}

/**
Expand Down Expand Up @@ -382,6 +432,7 @@ function mediaPrimaryImage(mediaId: string): string {
async function handleAudioPlayback(): Promise<void> {
// sometimes the buttons are not created fast enough
try {
presenceData.type = ActivityType.Listening;
const audioElement = document.querySelector<HTMLAudioElement>("audio"),
regexResult = /\/Audio\/(\w+)\/universal/.exec(audioElement.src);

Expand All @@ -399,15 +450,17 @@ async function handleAudioPlayback(): Promise<void> {
// some songs might not have albumart
document.querySelector<HTMLDivElement>(".nowPlayingBarImage").style
.backgroundImage
)
) {
presenceData.largeImageKey = mediaPrimaryImage(mediaId);
createImageBlob();
}

// playing
if (!audioElement.paused) {
presenceData.smallImageKey = PRESENCE_ART_ASSETS.play;
presenceData.smallImageText = "Playing";

if (await presence.getSetting<boolean>("showMediaTimestamps")) {
if (showMediaTimestamp) {
[presenceData.startTimestamp, presenceData.endTimestamp] =
presence.getTimestampsfromMedia(audioElement);
} else delete presenceData.endTimestamp;
Expand Down Expand Up @@ -460,24 +513,39 @@ const mediaInfoCache = new Map<string, MediaInfo>();
*/
async function obtainMediaInfo(itemId: string): Promise<MediaInfo> {
if (mediaInfoCache.has(itemId)) return mediaInfoCache.get(itemId);

let { AccessToken: accessToken } = ApiClient._serverInfo;
let ae, accessToken: string;
if (!version.includes("4.7")) {
ae = ApiClient._serverInfo as ServerLatest;
accessToken = ae?.Users?.[1]?.AccessToken;
} else {
ae = ApiClient._serverInfo as Server;
accessToken = ae?.AccessToken;
}

if (!accessToken) {
// refresh the ApiClient
ApiClient = await getApiClient();

({ AccessToken: accessToken } = ApiClient._serverInfo);
if (!version.includes("4.7")) {
ae = ApiClient._serverInfo as ServerLatest;
accessToken = ae?.Users?.[1]?.AccessToken;
} else {
ae = ApiClient._serverInfo as Server;
accessToken = ae?.AccessToken;
}
}

const res = await fetch(
`${embyBasenameURL()}emby/Users/${getUserId()}/Items/${itemId}?` +
`X-Emby-Client=${ApiClient._appName}&` +
`X-Emby-Device-Name=${ApiClient._deviceName}&` +
`X-Emby-Device-Id=${ApiClient._deviceId}&` +
`X-Emby-Client-Version=${ApiClient._appVersion}&` +
`X-Emby-Token=${accessToken}`
),
const url =
`${embyBasenameURL()}emby/Users/${getUserId()}/Items/${itemId.replace(
/\//gm,
""
)}?` +
`X-Emby-Client=${ApiClient._appName.replace(/ /gm, "_")}&` +
`X-Emby-Device-Name=${ApiClient._deviceName}&` +
`X-Emby-Device-Id=${ApiClient._deviceId}&` +
`X-Emby-Client-Version=${ApiClient._appVersion}&` +
`X-Emby-Token=${accessToken}`;

console.log(url.replace(/ /gm, "_"));

Check warning on line 547 in websites/E/Emby/presence.ts

View workflow job for this annotation

GitHub Actions / Compile and Lint

Unexpected console statement
const res = await fetch(url.replace(/ /gm, "_")),
mediaInfo: MediaInfo = await res.json();

mediaInfoCache.set(itemId, mediaInfo);
Expand All @@ -489,12 +557,15 @@ async function obtainMediaInfo(itemId: string): Promise<MediaInfo> {
* handleVideoPlayback - handles the presence when the user is using the video player
*/
async function handleVideoPlayback(): Promise<void> {
const videoPlayerPage = document.querySelector("[data-type='video-osd']");
const videoPlayerPage =
document.querySelector("[data-type='video-osd']") ||
document.querySelector(".htmlVideoPlayerContainer");

if (videoPlayerPage === null) {
// elements not loaded yet
return;
}
getApiClient();

const videoPlayerElem = document.querySelector<HTMLVideoElement>("video");

Expand All @@ -503,23 +574,31 @@ async function handleVideoPlayback(): Promise<void> {
subtitle,
largeImage = PRESENCE_ART_ASSETS.logo;

const regexResult = /\/Items\/(\d+)\//.exec(
document.querySelector<HTMLDivElement>(".pageTitle").style.backgroundImage
);
const regexResult =
/\/Items\/(\d+)\//.exec(
document.querySelector<HTMLDivElement>(".pageTitle")?.style
.backgroundImage
) ||
/\/Items\/(\d+)\//.exec(
document.querySelector<HTMLDivElement>(".itemBackdrop")?.style
?.backgroundImage
) ||
/\/[0-9]+\//.exec(
document.querySelector<HTMLVideoElement>(".htmlVideoPlayerContainer")?.src
);

if (!regexResult) {
presence.error("Could not obtain video itemId");
return;
}

const mediaInfo = await obtainMediaInfo(regexResult[1]);
const mediaInfo = await obtainMediaInfo(regexResult[1] || regexResult[0]);

// display generic info
if (!mediaInfo) {
if (!mediaInfo || typeof mediaInfo === "string") {
title = "Watching unknown content";
subtitle = "No metadata could be obtained";
} else if (typeof mediaInfo === "string") return;
else {
} else {
switch (mediaInfo.Type) {
case "Movie":
title = "Watching a Movie";
Expand All @@ -546,34 +625,36 @@ async function handleVideoPlayback(): Promise<void> {
}

presenceData.largeImageKey = largeImage;
createImageBlob();

// watching live tv
if (mediaInfo && mediaInfo.Type === "TvChannel") {
presenceData.type = ActivityType.Watching;
presenceData.smallImageKey = PRESENCE_ART_ASSETS.live;
presenceData.smallImageText = "Live TV";

// playing
} else if (!videoPlayerElem.paused) {
presenceData.type = ActivityType.Watching;
presenceData.smallImageKey = PRESENCE_ART_ASSETS.play;
presenceData.smallImageText = "Playing";

if (await presence.getSetting<boolean>("showMediaTimestamps")) {
if (showMediaTimestamp) {
[presenceData.startTimestamp, presenceData.endTimestamp] =
presence.getTimestampsfromMedia(videoPlayerElem);
} else delete presenceData.endTimestamp;

// paused
} else {
presenceData.type = ActivityType.Watching;
presenceData.smallImageKey = PRESENCE_ART_ASSETS.pause;
presenceData.smallImageText = "Paused";

delete presenceData.endTimestamp;
}
}

presenceData.details = title;
presenceData.state = subtitle;

if (!presenceData.state) delete presenceData.state;
}

Expand Down Expand Up @@ -731,10 +812,10 @@ async function handleWebClient(): Promise<void> {
await handleItemDetails();
break;

case "videoosd/videoosd.html":
case "videoosd/videoosd.html": {
await handleVideoPlayback();
break;

}
default:
if (path.substr(0, 3) !== "dlg") presence.info(`path: ${path}`);
}
Expand Down Expand Up @@ -763,10 +844,18 @@ async function setDefaultsToPresence(): Promise<void> {
* updateData - tick function, this is called several times a second by UpdateData event
*/
async function updateData(): Promise<void> {
showMediaTimestamp = await presence.getSetting<boolean>(
"showMediaTimestamps"
);
await setDefaultsToPresence();

let showPresence = false;

const tempversion =
ApiClient?._appVersion ?? (await getApiClient())?._appVersion;

if (!version || version !== tempversion) version = tempversion;

// we are on the official emby page
if (location.host.toLowerCase() === EMBY_URL) {
showPresence = true;
Expand All @@ -787,6 +876,9 @@ async function updateData(): Promise<void> {

// if emby is detected init/update the presence status
if (showPresence) {
if (!presenceData.largeImageKey)
presenceData.largeImageKey = PRESENCE_ART_ASSETS.logo;

if (!presenceData.details) presence.setActivity();
else presence.setActivity(presenceData);
}
Expand Down Expand Up @@ -830,10 +922,15 @@ async function init(): Promise<void> {
presence = new Presence({
clientId: "671807692297207828",
});

if (isWebClient) presence.info("Emby web client detected");

presence.on("UpdateData", updateData);
if (isWebClient) presence.info("Emby web client detected");
}
}

function isPrivateIP(ip: string): boolean {
return /^http:\/\/(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|127\.0\.0\.1|localhost)/.test(
ip
);
}

init();

0 comments on commit 63705e2

Please sign in to comment.