feat: Emby support (#234)

* fix: support credential extraction from Emby

* fix: minor compatibility with emby for retrieving albums

* fix: rename credentials and save credentials type

* fix: weird issue when changing libraries

* fix: correctly map platform names in auth header

* chore: properly carry over old settings

* fix: only enable playlists on jellyfin

* fix: remove jellyfin mentions

* fix: incorporate jellyfin and emby as mentions
This commit is contained in:
Lei Nelissen
2024-07-25 23:37:00 +02:00
committed by GitHub
parent c15f8fe1fc
commit a6452f0a5e
35 changed files with 294 additions and 168 deletions

View File

@@ -5,9 +5,50 @@ import { AppState } from '@/store';
interface Props {
serverUrl: string;
onCredentialsRetrieved: (credentials: AppState['settings']['jellyfin']) => void;
onCredentialsRetrieved: (credentials: AppState['settings']['credentials']) => void;
}
type CredentialEventData = {
credentials: {
Servers: {
ManualAddress: string,
ManualAddressOnly: boolean,
IsLocalServer: boolean,
DateLastAccessed: number,
LastConnectionMode: number,
Type: string,
Name: string,
Id: string,
UserId: string | null,
AccessToken: string | null,
Users: {
UserId: string,
AccessToken: string,
}[]
LocalAddress: string,
RemoteAddress: string,
}[]
},
deviceId: string,
type: 'emby',
} | {
credentials: {
Servers: {
ManualAddress: string,
manualAddressOnly: boolean,
DateLastAccessed: number,
LastConnectionMode: number,
Name: string,
Id: string,
UserId: string | null,
AccessToken: string | null,
LocalAddress: string,
}[]
},
deviceId: string,
type: 'jellyfin',
} | undefined;
class CredentialGenerator extends Component<Props> {
ref = createRef<WebView>();
@@ -18,12 +59,18 @@ class CredentialGenerator extends Component<Props> {
checkIfCredentialsAreThere = debounce(() => {
// Inject some javascript to check if the credentials can be extracted
// from localstore
// from localstore. We simultaneously attempt to extract credentials for
// any back-end.
this.ref.current?.injectJavaScript(`
try {
let credentials = JSON.parse(window.localStorage.getItem('jellyfin_credentials'));
let deviceId = window.localStorage.getItem('_deviceId2');
window.ReactNativeWebView.postMessage(JSON.stringify({ credentials, deviceId }))
window.ReactNativeWebView.postMessage(JSON.stringify({ credentials, deviceId, type: 'jellyfin' }))
} catch(e) { }; true;
try {
let credentials = JSON.parse(window.localStorage.getItem('servercredentials3'));
let deviceId = window.localStorage.getItem('_deviceId2');
window.ReactNativeWebView.postMessage(JSON.stringify({ credentials, deviceId, type: 'emby' }))
} catch(e) { }; true;
`);
}, 500);
@@ -35,36 +82,73 @@ class CredentialGenerator extends Component<Props> {
}
// Parse the content
const data = JSON.parse(event.nativeEvent.data);
const data = JSON.parse(event.nativeEvent.data) as CredentialEventData;
if (__DEV__) {
console.log('Received credential event data: ', JSON.stringify(data));
}
if (!data.deviceId
|| !data.credentials?.Servers?.length
|| !data.credentials?.Servers[0]?.UserId
|| !data.credentials?.Servers[0]?.AccessToken) {
// Since Jellyfin and Emby are similar, we'll attempt to extract the
// credentials in a generic way.
let userId: string | undefined, accessToken: string | undefined;
// GUARD: Attempt to extract emby format credentials
if (data?.type === 'emby'
&& data.credentials?.Servers?.length
&& data.credentials?.Servers[0]?.Users?.length
) {
userId = data.credentials.Servers[0].Users[0].UserId;
accessToken = data.credentials.Servers[0].Users[0].AccessToken;
// GUARD: Attempt to extract jellyfin format credentials
} else if (data?.type === 'jellyfin'
&& data.credentials?.Servers?.length
) {
userId = data.credentials.Servers[0].UserId || undefined;
accessToken = data.credentials.Servers[0].AccessToken || undefined;
}
// We can extract the deviceId and server address in the same way for
// both Jellyfin and Emby.
const deviceId = data?.deviceId;
const address = data?.credentials?.Servers?.length
&& data?.credentials.Servers[0].ManualAddress;
// GUARD: log extract credentials in dev
if (__DEV__) {
console.log('Extracted the following credentials:', { userId, accessToken, deviceId, address });
}
// GUARD: Check that all the required credentials are available
if (!userId || !accessToken || !deviceId || !address) {
if (__DEV__) {
console.error('Failed to extract credentials from event');
}
return;
}
const { credentials: { Servers: [ credentials ] }, deviceId } = data;
// Attempt to perform a request using the credentials to see if they're
// good
const response = await fetch(`${credentials.ManualAddress}/Users/Me`, {
const response = await fetch(`${address}/Users/${userId}`, {
headers: {
'X-Emby-Authorization': `MediaBrowser Client="", Device="", DeviceId="", Version="", Token="${credentials.AccessToken}"`
'X-Emby-Authorization': `MediaBrowser Client="", Device="", DeviceId="", Version="", Token="${accessToken}"`
}
});
// GUARD: The request must succeed
if (response.status !== 200) {
if (__DEV__) {
const body = await response.text();
console.error('Failed to retrieve user object using credentials:', response.status, body);
}
return;
}
// If a message is received, the credentials should be there
this.props.onCredentialsRetrieved({
uri: credentials.ManualAddress,
user_id: credentials.UserId,
access_token: credentials.AccessToken,
uri: address,
user_id: userId,
access_token: accessToken,
device_id: deviceId,
type: data.type,
});
};

View File

@@ -22,7 +22,7 @@ export default function SetJellyfinServer() {
const navigation = useNavigation();
// Save creedentials to store and close the modal
const saveCredentials = useCallback((credentials: AppState['settings']['jellyfin']) => {
const saveCredentials = useCallback((credentials: AppState['settings']['credentials']) => {
if (credentials) {
dispatch(setJellyfinCredentials(credentials));
navigation.dispatch(StackActions.popToTop());
@@ -39,7 +39,7 @@ export default function SetJellyfinServer() {
) : (
<View style={{ padding: 20, flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text>
{t('set-jellyfin-server-instruction')}
{t('set-server-instruction')}
</Text>
<Input
placeholder="https://jellyfin.yourserver.io/"
@@ -51,7 +51,7 @@ export default function SetJellyfinServer() {
style={[ defaultStyles.input, { width: '100%' } ]}
/>
<Button
title={t('set-jellyfin-server')}
title={t('set-server')}
onPress={() => setIsLogginIn(true)}
disabled={!serverUrl?.length}
color={defaultStyles.themeColor.color}