From 596bf3a3785129fc0ccebab26754fc5f7673a56e Mon Sep 17 00:00:00 2001 From: sel10ut <45976019+sel10ut@users.noreply.github.com> Date: Sat, 29 Jun 2024 00:26:01 +0300 Subject: [PATCH 1/2] fix(jellyfin): allow multiple sessions from the same client type Allow multiple sessions from the same user with different instances. Instead of sending a hard-coded string, send a randomly generated string `deviceId`, which already exists and is created for each new installation. --- src/renderer/api/jellyfin/jellyfin-controller.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index ff07ebc5f..48376a47c 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -64,6 +64,7 @@ import isElectron from 'is-electron'; import { ServerFeature } from '/@/renderer/api/features-types'; import { VersionInfo, getFeatures } from '/@/renderer/api/utils'; import chunk from 'lodash/chunk'; +import { useAuthStore } from '/@/renderer/store'; const formatCommaDelimitedString = (value: string[]) => { return value.join(','); @@ -109,9 +110,9 @@ const authenticate = async ( Username: body.username, }, headers: { - 'x-emby-authorization': `MediaBrowser Client="Feishin", Device="${getHostname()}", DeviceId="Feishin-${getHostname()}-${encodeURIComponent( - body.username, - )}", Version="${packageJson.version}"`, + 'x-emby-authorization': `MediaBrowser Client="Feishin", Device="${getHostname()}", DeviceId="${ + useAuthStore.getState().deviceId + }", Version="${packageJson.version}"`, }, }); From ba64f4c4672a2f8ae247ee6371f0a2cd21ba66e2 Mon Sep 17 00:00:00 2001 From: sel10ut <45976019+sel10ut@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:51:41 +0300 Subject: [PATCH 2/2] refactor(jellyfin): migrate auth method --- src/renderer/api/jellyfin/jellyfin-api.ts | 16 ++++++--- .../api/jellyfin/jellyfin-controller.ts | 33 ------------------- .../api/jellyfin/jellyfin-normalize.ts | 2 +- src/renderer/api/utils.ts | 26 +++++++++++++++ 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/renderer/api/jellyfin/jellyfin-api.ts b/src/renderer/api/jellyfin/jellyfin-api.ts index 5dbae8a74..f29a01c55 100644 --- a/src/renderer/api/jellyfin/jellyfin-api.ts +++ b/src/renderer/api/jellyfin/jellyfin-api.ts @@ -6,8 +6,9 @@ import qs from 'qs'; import { ServerListItem } from '/@/renderer/api/types'; import omitBy from 'lodash/omitBy'; import { z } from 'zod'; -import { authenticationFailure } from '/@/renderer/api/utils'; +import { authenticationFailure, getClientType } from '/@/renderer/api/utils'; import i18n from '/@/i18n/i18n'; +import packageJson from '../../../../package.json'; const c = initContract(); @@ -24,9 +25,6 @@ export const contract = c.router({ }, authenticate: { body: jfType._parameters.authenticate, - headers: z.object({ - 'X-Emby-Authorization': z.string(), - }), method: 'POST', path: 'users/authenticatebyname', responses: { @@ -333,6 +331,12 @@ const parsePath = (fullPath: string) => { }; }; +export const createAuthHeader = (): string => { + return `MediaBrowser Client="Feishin", Device="${getClientType()}", DeviceId="${ + useAuthStore.getState().deviceId + }", Version="${packageJson.version}"`; +}; + export const jfApiClient = (args: { server: ServerListItem | null; signal?: AbortSignal; @@ -359,7 +363,9 @@ export const jfApiClient = (args: { data: body, headers: { ...headers, - ...(token && { 'X-MediaBrowser-Token': token }), + ...(token + ? { Authorization: createAuthHeader().concat(`, Token="${token}"`) } + : { Authorization: createAuthHeader() }), }, method: method as Method, params, diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index 48376a47c..2b9081aa3 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -57,44 +57,16 @@ import { import { jfApiClient } from '/@/renderer/api/jellyfin/jellyfin-api'; import { jfNormalize } from './jellyfin-normalize'; import { jfType } from '/@/renderer/api/jellyfin/jellyfin-types'; -import packageJson from '../../../../package.json'; import { z } from 'zod'; import { JFSongListSort, JFSortOrder } from '/@/renderer/api/jellyfin.types'; -import isElectron from 'is-electron'; import { ServerFeature } from '/@/renderer/api/features-types'; import { VersionInfo, getFeatures } from '/@/renderer/api/utils'; import chunk from 'lodash/chunk'; -import { useAuthStore } from '/@/renderer/store'; const formatCommaDelimitedString = (value: string[]) => { return value.join(','); }; -function getHostname(): string { - if (isElectron()) { - return 'Desktop Client'; - } - const agent = navigator.userAgent; - switch (true) { - case agent.toLowerCase().indexOf('edge') > -1: - return 'Microsoft Edge'; - case agent.toLowerCase().indexOf('edg/') > -1: - return 'Edge Chromium'; // Match also / to avoid matching for the older Edge - case agent.toLowerCase().indexOf('opr') > -1: - return 'Opera'; - case agent.toLowerCase().indexOf('chrome') > -1: - return 'Chrome'; - case agent.toLowerCase().indexOf('trident') > -1: - return 'Internet Explorer'; - case agent.toLowerCase().indexOf('firefox') > -1: - return 'Firefox'; - case agent.toLowerCase().indexOf('safari') > -1: - return 'Safari'; - default: - return 'PC'; - } -} - const authenticate = async ( url: string, body: { @@ -109,11 +81,6 @@ const authenticate = async ( Pw: body.password, Username: body.username, }, - headers: { - 'x-emby-authorization': `MediaBrowser Client="Feishin", Device="${getHostname()}", DeviceId="${ - useAuthStore.getState().deviceId - }", Version="${packageJson.version}"`, - }, }); if (res.status !== 200) { diff --git a/src/renderer/api/jellyfin/jellyfin-normalize.ts b/src/renderer/api/jellyfin/jellyfin-normalize.ts index a17eaa76e..e1ae98134 100644 --- a/src/renderer/api/jellyfin/jellyfin-normalize.ts +++ b/src/renderer/api/jellyfin/jellyfin-normalize.ts @@ -30,7 +30,7 @@ const getStreamUrl = (args: { `?userId=${server?.userId}` + `&deviceId=${deviceId}` + '&audioCodec=aac' + - `&api_key=${server?.credential}` + + `&apiKey=${server?.credential}` + `&playSessionId=${deviceId}` + '&container=opus,mp3,aac,m4a,m4b,flac,wav,ogg' + '&transcodingContainer=ts' + diff --git a/src/renderer/api/utils.ts b/src/renderer/api/utils.ts index 3034d5c4f..314185ae9 100644 --- a/src/renderer/api/utils.ts +++ b/src/renderer/api/utils.ts @@ -1,4 +1,5 @@ import { AxiosHeaders } from 'axios'; +import isElectron from 'is-electron'; import semverCoerce from 'semver/functions/coerce'; import semverGte from 'semver/functions/gte'; import { z } from 'zod'; @@ -99,4 +100,29 @@ export const getFeatures = ( return features; }; +export const getClientType = (): string => { + if (isElectron()) { + return 'Desktop Client'; + } + const agent = navigator.userAgent; + switch (true) { + case agent.toLowerCase().indexOf('edge') > -1: + return 'Microsoft Edge'; + case agent.toLowerCase().indexOf('edg/') > -1: + return 'Edge Chromium'; // Match also / to avoid matching for the older Edge + case agent.toLowerCase().indexOf('opr') > -1: + return 'Opera'; + case agent.toLowerCase().indexOf('chrome') > -1: + return 'Chrome'; + case agent.toLowerCase().indexOf('trident') > -1: + return 'Internet Explorer'; + case agent.toLowerCase().indexOf('firefox') > -1: + return 'Firefox'; + case agent.toLowerCase().indexOf('safari') > -1: + return 'Safari'; + default: + return 'PC'; + } +}; + export const SEPARATOR_STRING = ' ยท ';