123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933 |
- /*
- * The interfaces may have some things missing,
- * I've tried to set as many properties as I could find.
- */
- //#region Interfaces
- interface ApiClient {
- enableAutomaticBitrateDetection: boolean;
- enableAutomaticNetworking: boolean;
- lastDetectedBitrate: number;
- lastDetectedBitrateTime: number; // timestamp
- lastFetch: number; // timestamp
- lastPlaybackProgressReport: number;
- lastPlaybackProgressReportTicks: number;
- manualAddressOnly: boolean;
- _appName: string;
- _appVersion: string;
- _currentUser: {
- Configuration: {
- AudioLanguagePreference: string;
- DisplayCollectionsView: boolean;
- DisplayMissingEpisodes: boolean;
- EnableLocalPassword: boolean;
- EnableNextEpisodeAutoPlay: boolean;
- HidePlayedInLatest: boolean;
- OrderedViews: string[];
- PlayDefaultAudioTrack: boolean;
- RememberAudioSelections: boolean;
- RememberSubtitleSelections: boolean;
- SubtitleLanguagePreference: string;
- SubtitleMode: string;
- };
- HasConfiguredEasyPassword: boolean;
- HasConfiguredPassword: boolean;
- HasPassword: boolean;
- Id: string;
- LastActivityDate: string; // date, ex: "2020-05-30T21:51:23.9732162Z"
- LastLoginDate: string; // date, ex: "2020-05-30T21:51:23.9732162Z"
- Name: string;
- Policy: {
- AuthenticationProviderId: string;
- EnableAllChannels: boolean;
- EnableAllDevices: boolean;
- EnableAllFolders: boolean;
- EnableAudioPlaybackTranscoding: boolean;
- EnableContentDeletion: boolean;
- EnableContentDownloading: boolean;
- EnableLiveTvAccess: boolean;
- EnableLiveTvManagement: boolean;
- EnableMediaConversion: boolean;
- EnableMediaPlayback: boolean;
- EnablePlaybackRemuxing: boolean;
- EnablePublicSharing: boolean;
- EnableRemoteAccess: boolean;
- EnableRemoteControlOfOtherUsers: boolean;
- EnableSharedDeviceControl: boolean;
- EnableSyncTranscoding: boolean;
- EnableUserPreferenceAccess: boolean;
- EnableVideoPlaybackTranscoding: boolean;
- ForceRemoteSourceTranscoding: boolean;
- InvalidLoginAttemptCount: boolean;
- IsAdministrator: boolean;
- IsDisabled: boolean;
- IsHidden: boolean;
- LoginAttemptsBeforeLockout: number;
- PasswordResetProviderId: string;
- RemoteClientBitrateLimit: number;
- };
- PrimaryImageAspectRatio: number;
- PrimaryImageTag: string;
- ServerId: string;
- };
- _deviceId: string;
- _deviceName: string;
- _endPointInfo: {
- IsInNetwork: boolean;
- IsLocal: boolean;
- };
- _serverAddress: string;
- _serverInfo: Server;
- _serverVersion: string;
- _webSocket: {
- binaryType: string;
- bufferedAmount: number;
- extensions: string;
- protocol: string;
- readyState: number;
- url: string;
- };
- }
- interface MediaStream {
- Codec: string;
- TimeBase: string;
- CodecTimeBase: string;
- VideoRange: string;
- DisplayTitle: string;
- IsInterlaced: boolean;
- BitRate: number;
- RefFrames: number;
- IsDefault: boolean;
- IsForced: boolean;
- Height: number;
- Width: number;
- AverageFrameRate: number;
- RealFrameRate: number;
- Profile: string;
- Type: string;
- AspectRatio: string;
- Index: number;
- IsExternal: boolean;
- IsTextSubtitleStream: boolean;
- SupportsExternalStream: boolean;
- PixelFormat: string;
- Level: number;
- }
- interface MediaSource {
- Protocol: string;
- Id: string;
- Path: string;
- Type: string;
- Container: string;
- Size: number;
- Name: string;
- IsRemote: boolean;
- ETag: string;
- RunTimeTicks: number;
- ReadAtNativeFramerate: boolean;
- IgnoreDts: boolean;
- IgnoreIndex: boolean;
- GenPtsInput: boolean;
- SupportsTranscoding: true;
- SupportsDirectStream: boolean;
- SupportsDirectPlay: boolean;
- IsInfiniteStream: boolean;
- RequiresOpening: boolean;
- RequiresClosing: boolean;
- RequiresLooping: boolean;
- SupportsProbing: true;
- VideoType: string;
- MediaStreams: MediaStream[];
- MediaAttachments: [];
- Formats: [];
- Bitrate: number;
- RequiredHttpHeaders: unknown;
- DefaultAudioStreamIndex: number;
- }
- interface ExternalUrl {
- Name: string;
- Url: string;
- }
- interface Person {
- Name: string;
- Id: string;
- Role: string;
- Type: string;
- PrimaryImageTag: string;
- }
- interface UserData {
- PlaybackPositionTicks: number;
- PlayCount: number;
- IsFavorite: boolean;
- LastPlayedDate: string; // date, ex: "2020-05-30T21:51:23.9732162Z"
- Played: boolean;
- Key: string;
- }
- interface Chapter {
- StartPositionTicks: number;
- Name: string;
- ImageDateModified: string; // date, ex: "2020-05-30T21:51:23.9732162Z"
- }
- interface MediaInfo {
- AlbumArtist: string;
- AlbumArtists: { Name: string; Id: string }[];
- AlbumId: string;
- AlbumPrimaryImageTag: string;
- ArtistsItems: { Name: string; Id: string }[];
- Artists: string[];
- Name: string;
- OriginalTitle: string;
- ServerId: string;
- Id: string;
- Etag: string;
- DateCreated: string; // date, ex: "2020-05-30T21:51:23.9732162Z"
- CanDelete: boolean;
- CanDownload: boolean;
- HasSubtitles: boolean;
- Container: string;
- SortName: string;
- PremiereDate: string; // date, ex: "2020-05-30T21:51:23.9732162Z"
- ExternalUrls: ExternalUrl[];
- MediaSources: MediaSource[];
- Path: string;
- EnableMediaSourceDisplay: boolean;
- Overview: string;
- // TagLines: Array;
- // Genres: Array;
- CommunityRating: number;
- RunTimeTicks: number;
- PlayAccess: string;
- ProductionYear: number;
- IndexNumber: number;
- ParentIndexNumber: number;
- // RemoteTrailers: Array;
- ProviderIds: {
- Tvdb?: number;
- };
- IsHD: boolean;
- IsFolder: boolean;
- ParentId: string;
- Type:
- | "Audio"
- | "MusicAlbum"
- | "MusicArtist"
- | "Movie"
- | "Series"
- | "Season"
- | "Episode"
- | "TvChannel"
- | "Person";
- People: Person[];
- // Studios: Array;
- // GenreItems: Array;
- ParentBackdropItemId: string;
- ParentBackdropImageTags: string[];
- LocalTrailerCount: number;
- UserData: UserData;
- RecursiveItemCount: number;
- Status: string;
- SeriesName: string;
- SeriesId: string;
- SeasonId: string;
- SpecialFeatureCount: number;
- DisplayPreferencesId: string;
- // Tags: Array;
- PrimaryImageAspectRatio: number;
- SeriesPrimaryImageTag: string;
- SeasonName: string;
- MediaStreams: MediaStream[];
- VideoType: string;
- ImageTags: {
- Primary: string;
- };
- // BackdropImageTags: Array;
- // ScreenshotImageTags: Array;
- SeriesStudio: string;
- Chapters: Chapter[];
- LocationType: string;
- MediaType: string;
- // LockedFields: Array;
- LockData: boolean;
- Width: number;
- Height: number;
- }
- interface Server {
- AccessToken: string;
- DateLastAccessed: number; // timestamp
- Id: string;
- IsLocalServer: boolean;
- LastConnectionMode: number;
- LocalAddress: string;
- ManualAddress: string;
- Name: string;
- RemoteAddress: string;
- Type: "Server";
- UserId: string;
- manualAddressOnly: boolean;
- }
- // #endregion
- const enum Assets {
- logo = "https://cdn.rcd.gg/PreMiD/websites/J/Jellyfin/assets/logo.png",
- }
- const JELLYFIN_URL = "jellyfin.org",
- // all the presence art assets uploaded to discord
- presenceData: PresenceData = {
- largeImageKey: Assets.logo,
- startTimestamp: Math.floor(Date.now() / 1000),
- };
- let ApiClient: ApiClient,
- presence: Presence,
- wasLogin = false;
- /**
- * Obtain the base name Url of the server
- */
- function jellyfinBasenameUrl(): string {
- const { pathname } = location;
- return `${location.origin}${pathname.replace(
- pathname.split("/").slice(-2).join("/"),
- ""
- )}`;
- }
- /**
- * Obtain the url of the primary image of a media given its id
- */
- function mediaPrimaryImage(mediaInfo: MediaInfo): string {
- let mediaId: string;
- switch (mediaInfo.Type) {
- case "Episode":
- mediaId = mediaInfo.SeriesId;
- break;
- case "Audio":
- mediaId = mediaInfo.AlbumId;
- break;
- default:
- mediaId = mediaInfo.Id;
- }
- return `${jellyfinBasenameUrl()}Items/${mediaId}/Images/Primary?fillHeight=256&fillWidth=256`;
- }
- /**
- * Handle the presence when the audio player is active
- */
- async function handleAudioPlayback(): Promise<void> {
- const regexResult = /\/Audio\/(\w+)\/universal/.exec(
- document.querySelector("audio").src
- );
- if (!regexResult) {
- presence.error("Could not obtain audio itemId");
- return;
- }
- await setPresenceByMediaId(regexResult[1]);
- }
- /**
- * Handle the presence while the user is in the official website
- */
- function handleOfficialWebsite(): void {
- presenceData.details = "At jellyfin.org";
- switch (location.pathname) {
- case "/":
- presenceData.state = "On landing page";
- break;
- case "/posts/":
- presenceData.state = "Reading the latest posts";
- presenceData.smallImageKey = Assets.Reading;
- break;
- case "/clients/":
- presenceData.state = "Checking clients";
- presenceData.smallImageKey = Assets.Search;
- break;
- case "/downloads/":
- presenceData.state = "On downloads";
- presenceData.smallImageKey = Assets.Downloading;
- break;
- case "/contribute/":
- presenceData.state = "Learning how to contribute";
- break;
- case "/contact/":
- presenceData.state = "On contact page";
- break;
- default:
- // reading the docs
- if (location.pathname.indexOf("/docs/") === 0) {
- presenceData.state = `Reading the docs: ${document.title
- .split("|")[0]
- .trim()}`;
- presenceData.smallImageKey = Assets.Reading;
- }
- }
- }
- /**
- * Obtain the authenticated user id
- */
- function getUserId(): string {
- try {
- return ApiClient._currentUser.Id;
- } catch (e) {
- const servers: Server[] = JSON.parse(
- localStorage.getItem("jellyfin_credentials")
- ).Servers;
- return (
- servers.length === 1
- ? servers[0]
- : servers.find(
- (s: Server) =>
- s.Id ===
- new URLSearchParams(location.hash.split("?")[1]).get("serverId")
- )
- ).UserId;
- }
- }
- /**
- * Cache performed mediaInfo
- */
- const mediaInfoCache = new Map<string, MediaInfo>();
- /**
- * Obtain media info given an itemId
- */
- async function obtainMediaInfo(itemId: string): Promise<MediaInfo> {
- if (mediaInfoCache.has(itemId)) return mediaInfoCache.get(itemId);
- const res = await fetch(
- `${jellyfinBasenameUrl()}Users/${getUserId()}/Items/${itemId}`,
- {
- credentials: "include",
- headers: {
- "x-emby-authorization":
- `MediaBrowser Client="${ApiClient._appName}",` +
- `Device="${ApiClient._deviceName}",` +
- `DeviceId="${ApiClient._deviceId}",` +
- `Version="${ApiClient._appVersion}",` +
- `Token="${ApiClient._serverInfo.AccessToken}"`,
- },
- }
- ),
- mediaInfo: MediaInfo = await res.json();
- mediaInfoCache.set(itemId, mediaInfo);
- return mediaInfoCache.get(itemId);
- }
- /**
- * Cache performed media searches
- */
- const searchMediaCache = new Map<string, MediaInfo[]>(),
- uploadedMediaCache = new Map<string, string>();
- /**
- * Search Movie and Series given a term
- */
- async function searchMedia(searchTerm: string): Promise<MediaInfo[]> {
- if (searchMediaCache.has(searchTerm)) return searchMediaCache.get(searchTerm);
- if (/-[ ]S[0-9]+:E[0-9]+[ ]-/.test(searchTerm))
- searchTerm = searchTerm.split(" - ").pop();
- // The API does not like the year in the search term
- searchTerm = searchTerm.replace(/\([0-9]{4}\)/, "").trim();
- const res = await fetch(
- `${jellyfinBasenameUrl()}Users/${getUserId()}/Items/?searchTerm=${searchTerm}` +
- "&IncludePeople=false&IncludeMedia=true&IncludeGenres=false&IncludeStudios=false" +
- "&IncludeArtists=false&IncludeItemTypes=Movie,Episode&Limit=3" +
- "&Fields=PrimaryImageAspectRatio%2CCanDelete%2CBasicSyncInfo%2CMediaSourceCount" +
- "&Recursive=true&EnableTotalRecordCount=false&ImageTypeLimit=1",
- {
- credentials: "include",
- headers: {
- "x-emby-authorization":
- `MediaBrowser Client="${ApiClient._appName}",` +
- `Device="${ApiClient._deviceName}",` +
- `DeviceId="${ApiClient._deviceId}",` +
- `Version="${ApiClient._appVersion}",` +
- `Token="${ApiClient._serverInfo.AccessToken}"`,
- },
- }
- ),
- resJson = await res.json();
- searchMediaCache.set(searchTerm, resJson.Items);
- return searchMediaCache.get(searchTerm);
- }
- /**
- * Handles the presence when the user is using the video player
- */
- async function handleVideoPlayback(): Promise<void> {
- if (!document.querySelector("#videoOsdPage")) {
- // elements not loaded yet
- return;
- }
- // title on the header
- const [mediaInfo] = await searchMedia(
- document.querySelector<HTMLHeadingElement>("h3.pageTitle").textContent
- );
- if (mediaInfo) {
- await setPresenceByMediaId(mediaInfo.Id);
- return;
- }
- // display generic info
- presenceData.details = "Watching:";
- presenceData.state = "Unknown Content";
- if (!presenceData.state) delete presenceData.state;
- }
- /**
- * Handle the presence when the user is playing back content remotely
- */
- async function handleRemotePlayback(): Promise<void> {
- const [, mediaId] = /\/Items\/(\w+)\/Images/.exec(
- document.querySelector<HTMLDivElement>(".nowPlayingImage").style
- .backgroundImage
- );
- await setPresenceByMediaId(mediaId);
- }
- /**
- * Handle the presence when the user is viewing the details of an item
- */
- async function handleItemDetails(): Promise<void> {
- const data = await obtainMediaInfo(
- new URLSearchParams(location.hash.split("?")[1]).get("id")
- );
- if (!data) {
- presenceData.details = "Browsing details of an item";
- presenceData.state = "Could not get item details";
- } else if (typeof data === "string") return;
- else {
- presenceData.details = `Browsing details of: ${data.Name}`;
- switch (data.Type) {
- case "Movie":
- presenceData.state = `${data.Type} ─ ${data.OriginalTitle} (${data.ProductionYear})`;
- break;
- case "Series":
- presenceData.state = `${data.Type} ─ (${data.Status})`;
- break;
- case "Season":
- presenceData.state = `${data.Type} ─ ${data.SeriesName}`;
- break;
- case "Episode":
- presenceData.state = `${data.Type} ─ ${data.SeriesName} - ${data.SeasonName}`;
- break;
- case "Person": {
- let description = "Description not available";
- if (data.Overview) {
- description =
- data.Overview.substring(0, 40) +
- (data.Overview.length > 40 ? "..." : "");
- }
- presenceData.state = `${data.Type} ─ ${description}`;
- break;
- }
- case "MusicAlbum":
- presenceData.state = `${data.Type} ─ ${data.RecursiveItemCount} songs`;
- break;
- case "MusicArtist":
- case "TvChannel":
- presenceData.state = `${data.Type} ─ No further information available`;
- break;
- default:
- presenceData.state = "No further information available";
- }
- if (await presence.getSetting("showThumbnails"))
- presenceData.largeImageKey = mediaPrimaryImage(data);
- }
- }
- /**
- * Sets a presence based on the given multimedia mediaId
- */
- async function setPresenceByMediaId(mediaId: string): Promise<void> {
- const mediaInfo = await obtainMediaInfo(mediaId);
- let title: string, subtitle: string;
- switch (mediaInfo.Type) {
- case "Audio":
- presenceData.type = ActivityType.Listening;
- title = `Listening to ${mediaInfo.Name ?? "Unknown title"}`;
- subtitle = `By ${mediaInfo.AlbumArtist ?? "Unknown artist"}`;
- break;
- case "Movie":
- case "Series":
- title = `Watching ${mediaInfo.Type}`;
- subtitle = mediaInfo.Name;
- break;
- case "Episode":
- title = `Watching: ${mediaInfo.SeriesName}`;
- subtitle = `${/S[0-9]+:E[0-9]+/.exec(
- document.querySelector<HTMLHeadingElement>(".pageTitle").textContent
- )} - ${mediaInfo.Name}`;
- break;
- case "TvChannel":
- presenceData.smallImageKey = Assets.Live;
- presenceData.smallImageText = "Live TV";
- break;
- default:
- title = `Watching ${mediaInfo.Type}`;
- subtitle = mediaInfo.Name;
- }
- if (presenceData.type !== ActivityType.Listening)
- presenceData.type = ActivityType.Watching;
- if (await presence.getSetting("showThumbnails"))
- presenceData.largeImageKey = mediaPrimaryImage(mediaInfo);
- if (mediaInfo.Type !== "TvChannel") {
- const mediaElement =
- document.querySelector<HTMLMediaElement>("audio, video"),
- paused = mediaElement
- ? mediaElement.paused
- : document
- .querySelector<HTMLSpanElement>(
- ".nowPlayingBar .playPauseButton span"
- )
- .classList.contains("play_arrow");
- if (paused) {
- presenceData.smallImageKey = Assets.Pause;
- presenceData.smallImageText = "Paused";
- delete presenceData.endTimestamp;
- } else {
- presenceData.smallImageKey = Assets.Play;
- presenceData.smallImageText = "Playing";
- // TODO: worth setting timestamps on remote playback? Requires WS connection
- if (mediaElement && (await presence.getSetting("showMediaTimestamps"))) {
- [presenceData.startTimestamp, presenceData.endTimestamp] =
- presence.getTimestampsfromMedia(mediaElement);
- }
- }
- }
- presenceData.details = title;
- presenceData.state = subtitle;
- if (!presenceData.state) delete presenceData.state;
- }
- /**
- * Suspend execution of code for an interval, see <https://manpage.me/?q=sleep>
- */
- function sleep(ms: number): Promise<void> {
- return new Promise(res => {
- setTimeout(res, ms);
- });
- }
- /**
- * Refreshes the ApiClient object
- */
- async function loggedIn(): Promise<void> {
- let apiClient: ApiClient;
- do {
- await sleep(125);
- apiClient = await presence.getPageletiable<ApiClient>("ApiClient");
- } while (!apiClient._serverInfo.AccessToken);
- ApiClient = apiClient;
- }
- /**
- * Handle the presence while the user is in the web client
- */
- async function handleWebClient(): Promise<void> {
- const audioElement = document.body.querySelector<HTMLAudioElement>("audio"),
- nowPlayingBar = document.querySelector(".nowPlayingBar");
- // audio player active
- if (
- audioElement &&
- audioElement.classList.contains("mediaPlayerAudio") &&
- audioElement.src
- ) {
- await handleAudioPlayback();
- return;
- } else if (
- nowPlayingBar &&
- !nowPlayingBar.classList.contains("nowPlayingBar-hidden")
- ) {
- await handleRemotePlayback();
- return;
- }
- presenceData.details = "At web client";
- // obtain the path, on the example would return "login.html"
- // https://media.domain.tld/web/index.html#!/login.html?serverid=randomserverid
- const path = location.hash.split("?")[0].substring(2);
- if (path === "login.html") {
- wasLogin = true;
- presenceData.state = "Logging in";
- } else if (wasLogin) {
- loggedIn();
- wasLogin = false;
- }
- switch (path) {
- case "home.html":
- presenceData.state = "At home";
- break;
- case "search.html":
- presenceData.state = "Searching";
- presenceData.smallImageKey = Assets.Search;
- break;
- // user preferences
- case "mypreferencesmenu.html":
- case "myprofile.html": // profile
- case "mypreferencesdisplay.html": // display
- case "mypreferenceshome.html": // home
- case "mypreferencesplayback.html": // playback
- case "mypreferencessubtitles.html": // subtitles
- presenceData.state = "On user preferences";
- break;
- // admin dashboard
- case "dashboard.html": // server section
- case "dashboardgeneral.html": // general
- case "userprofiles.html": // user profiles
- case "useredit.html": // editing user profile
- case "userlibraryaccess.html": // editing user profile > library access
- case "userparentalcontrol.html": // editing user profile > parental control
- case "userpassword.html": // editing user profile > password
- case "library.html": // managing library
- case "librarydisplay.html": // library display settings
- case "metadataimages.html": // library metadata settings
- case "metadatanfo.html": // library NFO settings
- case "encodingsettings.html": // encoding settings > transcoding
- case "playbackconfiguration.html": // encoding settings > resume
- case "streamingsettings.html": // encoding settings > streaming
- case "devices.html": // devices
- case "device.html": // editing device
- case "serveractivity.html": // server activity
- case "dlnasettings.html": // dlna settings > settings
- case "dlnaprofiles.html": // dlna settings > profiles
- case "dlnaprofile.html": // dlna settings > add profile
- case "livetvstatus.html": // manage live tv
- case "livetvtuner.html": // add/manage tv tuner
- case "livetvguideprovider.html": // add/manage tv guide provider
- case "livetvsettings.html": // live tv settings (dvr) // advanced section
- case "networking.html": // networking
- case "apikeys.html": // api keys
- case "log.html": // logs
- case "notificationsettings.html": // notification settings
- case "installedplugins.html": // plugins
- case "availableplugins.html": // plugins catalog
- case "scheduledtasks.html": // scheduled tasks
- case "configurationpage": // plugins configuration page
- presenceData.state = "On admin dashboard";
- break;
- case "movies.html":
- presenceData.state = "Browsing movies";
- break;
- case "tv.html":
- presenceData.state = "Browsing tv series";
- break;
- case "music.html":
- presenceData.state = "Browsing music";
- break;
- case "livetv.html":
- presenceData.state = "Browsing Live TV";
- break;
- case "edititemmetadata.html":
- presenceData.state = "Editing media metadata";
- break;
- case "details":
- await handleItemDetails();
- break;
- case "video":
- await handleVideoPlayback();
- break;
- case "nowplaying.html":
- presenceData.state = "Viewing the audio playlist";
- break;
- }
- }
- /**
- * Sets default values to the presenceData object
- */
- async function setDefaultsToPresence(): Promise<void> {
- presenceData.largeImageKey = Assets.logo;
- if (presenceData.smallImageKey) delete presenceData.smallImageKey;
- if (presenceData.smallImageText) delete presenceData.smallImageText;
- if (presenceData.state) delete presenceData.state;
- if (presenceData.startTimestamp) delete presenceData.startTimestamp;
- if (isNaN(presenceData.endTimestamp as number))
- delete presenceData.endTimestamp;
- if ((await presence.getSetting<boolean>("showTimestamps")) === false)
- delete presenceData.startTimestamp;
- }
- /**
- * Initializes the ApiClient object
- */
- async function refreshApiClient(): Promise<void> {
- ApiClient ??= (
- await presence.getPageVariable<Record<"ApiClient", ApiClient>>("ApiClient")
- ).ApiClient;
- }
- /**
- * Imports the ApiClient variable and verifies that we are in the jellyfin web client
- */
- async function isJellyfinWebClient(): Promise<boolean> {
- if (!ApiClient) await refreshApiClient();
- if (
- ApiClient &&
- typeof ApiClient === "object" &&
- ApiClient._appName &&
- ApiClient._appName === "Jellyfin Web"
- )
- return true;
- return false;
- }
- /**
- * PreMiD tick function
- */
- async function updateData(): Promise<void> {
- await setDefaultsToPresence();
- let showPresence = false;
- // we are on the official jellyfin page
- if (location.host.toLowerCase() === JELLYFIN_URL) {
- showPresence = true;
- handleOfficialWebsite();
- // we are on the web client and has been verified
- } else if (await isJellyfinWebClient()) {
- showPresence = true;
- await handleWebClient();
- }
- // if jellyfin is detected init/update the presence status
- if (showPresence) {
- 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;
- };
- });
- }
- }
- if (!presenceData.details) presence.setActivity();
- else presence.setActivity(presenceData);
- }
- }
- 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
- );
- }
- /**
- * Check if the presence should be initialized, if so start doing the magic
- */
- async function init(): Promise<void> {
- let validPage = false,
- infoMessage;
- // jellyfin website
- if (location.host === JELLYFIN_URL) {
- validPage = true;
- infoMessage = "Jellyfin website detected";
- // web client
- } else {
- try {
- for (const server of JSON.parse(
- localStorage.getItem("jellyfin_credentials")
- ).Servers) {
- // user has accessed in the last 30 seconds, should be enough for slow connections
- if (
- Date.now() - new Date(server.DateLastAccessed).getTime() <
- 30 * 1000
- ) {
- validPage = true;
- infoMessage = "Jellyfin web client detected";
- }
- }
- } catch (e) {
- validPage = false;
- }
- }
- if (validPage) {
- presence = new Presence({
- clientId: "669359568391766018",
- });
- presence.info(infoMessage);
- presence.on("UpdateData", updateData);
- }
- }
- init();
|