Project Refactoring

This commit is contained in:
Aleksi Lassila
2023-07-09 15:50:04 +03:00
parent 56ef4ee865
commit 494a3bf85a
83 changed files with 319 additions and 276 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,128 @@
import createClient from 'openapi-fetch';
import type { paths } from '$lib/apis/jellyfin/jellyfin.generated';
import { PUBLIC_JELLYFIN_API_KEY, PUBLIC_JELLYFIN_URL } from '$env/static/public';
import { request } from '$lib/utils';
import type { DeviceProfile } from '$lib/apis/jellyfin/playback-profiles';
export const JELLYFIN_DEVICE_ID = 'Reiverr Client';
export const JELLYFIN_USER_ID = '75dcb061c9404115a7acdc893ea6bbbc';
export const JellyfinApi = createClient<paths>({
baseUrl: PUBLIC_JELLYFIN_URL,
headers: {
Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${PUBLIC_JELLYFIN_API_KEY}"`
}
});
export const getJellyfinContinueWatching = () =>
JellyfinApi.get('/Users/{userId}/Items/Resume', {
params: {
path: {
userId: JELLYFIN_USER_ID
},
query: {
limit: 8,
mediaTypes: ['Video'],
fields: ['ProviderIds']
}
}
}).then((r) => r.data?.Items);
export const getJellyfinItemByTmdbId = (tmdbId: string) =>
JellyfinApi.get('/Users/{userId}/Items', {
params: {
path: {
userId: JELLYFIN_USER_ID
},
query: {
hasTmdbId: true,
recursive: true,
isMovie: true,
fields: ['ProviderIds']
}
}
}).then((r) => r.data?.Items?.find((i) => i.ProviderIds?.Tmdb == tmdbId));
export const getJellyfinItem = (itemId: string) =>
JellyfinApi.get('/Users/{userId}/Items/{itemId}', {
params: {
path: {
itemId,
userId: JELLYFIN_USER_ID
}
}
}).then((r) => r.data);
export const requestJellyfinItemByTmdbId = () =>
request((tmdbId: string) => getJellyfinItemByTmdbId(tmdbId));
export const getJellyfinPlaybackInfo = (itemId: string, playbackProfile: DeviceProfile) =>
JellyfinApi.post('/Items/{itemId}/PlaybackInfo', {
params: {
path: {
itemId: itemId
},
query: {
userId: JELLYFIN_USER_ID,
startTimeTicks: 0,
autoOpenLiveStream: true,
maxStreamingBitrate: 140000000
}
},
body: {
DeviceProfile: playbackProfile
}
}).then((r) => ({
playbackUrl: r.data?.MediaSources?.[0]?.TranscodingUrl,
mediaSourceId: r.data?.MediaSources?.[0]?.Id,
playSessionId: r.data?.PlaySessionId
}));
export const reportJellyfinPlaybackStarted = (
itemId: string,
sessionId: string,
mediaSourceId: string,
audioStreamIndex?: number,
subtitleStreamIndex?: number
) =>
JellyfinApi.post('/Sessions/Playing', {
body: {
CanSeek: true,
ItemId: itemId,
PlaySessionId: sessionId,
MediaSourceId: mediaSourceId,
AudioStreamIndex: 1,
SubtitleStreamIndex: -1
}
});
export const reportJellyfinPlaybackProgress = (
itemId: string,
sessionId: string,
isPaused: boolean,
positionTicks: number
) =>
JellyfinApi.post('/Sessions/Playing/Progress', {
body: {
ItemId: itemId,
PlaySessionId: sessionId,
IsPaused: isPaused,
PositionTicks: Math.round(positionTicks),
CanSeek: true,
MediaSourceId: itemId
}
});
export const reportJellyfinPlaybackStopped = (
itemId: string,
sessionId: string,
positionTicks: number
) =>
JellyfinApi.post('/Sessions/Playing/Stopped', {
body: {
ItemId: itemId,
PlaySessionId: sessionId,
PositionTicks: Math.round(positionTicks),
MediaSourceId: itemId
}
});

View File

@@ -0,0 +1,104 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import { DlnaProfileType } from '@jellyfin/sdk/lib/generated-client';
import type { DirectPlayProfile } from '@jellyfin/sdk/lib/generated-client';
import { getSupportedMP4VideoCodecs } from './helpers/mp4-video-formats';
import { getSupportedMP4AudioCodecs } from './helpers/mp4-audio-formats';
import { hasMkvSupport } from './helpers/transcoding-formats';
import { getSupportedWebMAudioCodecs } from './helpers/webm-audio-formats';
import { getSupportedWebMVideoCodecs } from './helpers/webm-video-formats';
import { getSupportedAudioCodecs } from './helpers/audio-formats';
/**
* Returns a valid DirectPlayProfile for the current platform.
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns An array of direct play profiles for the current platform.
*/
export function getDirectPlayProfiles(
videoTestElement: HTMLVideoElement
): Array<DirectPlayProfile> {
const DirectPlayProfiles: DirectPlayProfile[] = [];
const webmVideoCodecs = getSupportedWebMVideoCodecs(videoTestElement);
const webmAudioCodecs = getSupportedWebMAudioCodecs(videoTestElement);
const mp4VideoCodecs = getSupportedMP4VideoCodecs(videoTestElement);
const mp4AudioCodecs = getSupportedMP4AudioCodecs(videoTestElement);
if (webmVideoCodecs.length > 0) {
DirectPlayProfiles.push({
Container: 'webm',
Type: DlnaProfileType.Video,
VideoCodec: webmVideoCodecs.join(','),
AudioCodec: webmAudioCodecs.join(',')
});
}
if (mp4VideoCodecs.length > 0) {
DirectPlayProfiles.push({
Container: 'mp4,m4v',
Type: DlnaProfileType.Video,
VideoCodec: mp4VideoCodecs.join(','),
AudioCodec: mp4AudioCodecs.join(',')
});
}
if (hasMkvSupport(videoTestElement) && mp4VideoCodecs.length > 0) {
DirectPlayProfiles.push({
Container: 'mkv',
Type: DlnaProfileType.Video,
VideoCodec: mp4VideoCodecs.join(','),
AudioCodec: mp4AudioCodecs.join(',')
});
}
const supportedAudio = [
'opus',
'mp3',
'mp2',
'aac',
'flac',
'alac',
'webma',
'wma',
'wav',
'ogg',
'oga'
];
for (const audioFormat of supportedAudio.filter((format) => getSupportedAudioCodecs(format))) {
DirectPlayProfiles.push({
Container: audioFormat,
Type: DlnaProfileType.Audio
});
if (audioFormat === 'opus' || audioFormat === 'webma') {
DirectPlayProfiles.push({
Container: 'webm',
Type: DlnaProfileType.Audio,
AudioCodec: audioFormat
});
}
// aac also appears in the m4a and m4b container
if (audioFormat === 'aac' || audioFormat === 'alac') {
DirectPlayProfiles.push(
{
Container: 'm4a',
AudioCodec: audioFormat,
Type: DlnaProfileType.Audio
},
{
Container: 'm4b',
AudioCodec: audioFormat,
Type: DlnaProfileType.Audio
}
);
}
}
return DirectPlayProfiles;
}

View File

@@ -0,0 +1,38 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import { isApple, isTizen, isTv, isWebOS } from '$lib/utils/browser-detection';
/**
* Determines if audio codec is supported
*/
export function getSupportedAudioCodecs(format: string): boolean {
let typeString;
if (format === 'flac' && isTv()) {
return true;
} else if (format === 'wma' && isTizen()) {
return true;
} else if (format === 'asf' && isTv()) {
return true;
} else if (format === 'opus') {
if (!isWebOS()) {
typeString = 'audio/ogg; codecs="opus"';
return !!document.createElement('audio').canPlayType(typeString).replace(/no/, '');
}
return false;
} else if (format === 'alac' && isApple()) {
return true;
} else if (format === 'webma') {
typeString = 'audio/webm';
} else if (format === 'mp2') {
typeString = 'audio/mpeg';
} else {
typeString = 'audio/' + format;
}
return !!document.createElement('audio').canPlayType(typeString).replace(/no/, '');
}

View File

@@ -0,0 +1,329 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import {
CodecType,
ProfileConditionType,
ProfileConditionValue
} from '@jellyfin/sdk/lib/generated-client';
import type { ProfileCondition, CodecProfile } from '@jellyfin/sdk/lib/generated-client';
import {
isApple,
isChromiumBased,
isEdge,
isMobile,
isPs4,
isTizen,
isTv,
isWebOS,
isXbox,
safariVersion
} from '$lib/utils/browser-detection';
/**
* Gets the max video bitrate
*
* @returns Returns the MaxVideoBitrate
*/
function getGlobalMaxVideoBitrate(): number | undefined {
let isTizenFhd = false;
if (
isTizen() &&
'webapis' in window &&
typeof window.webapis === 'object' &&
window.webapis &&
'productinfo' in window.webapis &&
typeof window.webapis.productinfo === 'object' &&
window.webapis.productinfo &&
'isUdPanelSupported' in window.webapis.productinfo &&
typeof window.webapis.productinfo.isUdPanelSupported === 'function'
) {
isTizenFhd = !window.webapis.productinfo.isUdPanelSupported();
}
// TODO: These values are taken directly from Jellyfin-web.
// The source of them needs to be investigated.
if (isPs4()) {
return 8_000_000;
}
if (isXbox()) {
return 12_000_000;
}
if (isTizen() && isTizenFhd) {
return 20_000_000;
}
}
/**
* Creates a profile condition object for use in device playback profiles.
*
* @param Property - Value for the property
* @param Condition - Condition that the property must comply with
* @param Value - Value to check in the condition
* @param IsRequired - Whether this property is required
* @returns - Constructed ProfileCondition object
*/
function createProfileCondition(
Property: ProfileConditionValue,
Condition: ProfileConditionType,
Value: string,
IsRequired = false
): ProfileCondition {
return {
Condition,
Property,
Value,
IsRequired
};
}
/**
* Gets the AAC audio codec profile conditions
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns - Array of ACC Profile conditions
*/
export function getAacCodecProfileConditions(
videoTestElement: HTMLVideoElement
): ProfileCondition[] {
const supportsSecondaryAudio = isTizen();
const conditions: ProfileCondition[] = [];
// Handle he-aac not supported
if (
!videoTestElement.canPlayType('video/mp4; codecs="avc1.640029, mp4a.40.5"').replace(/no/, '')
) {
// TODO: This needs to become part of the stream url in order to prevent stream copy
conditions.push(
createProfileCondition(
ProfileConditionValue.AudioProfile,
ProfileConditionType.NotEquals,
'HE-AAC'
)
);
}
if (!supportsSecondaryAudio) {
conditions.push(
createProfileCondition(
ProfileConditionValue.IsSecondaryAudio,
ProfileConditionType.Equals,
'false'
)
);
}
return conditions;
}
/**
* Gets an array with all the codec profiles that this client supports
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns - Array containing the different profiles for the client
*/
export function getCodecProfiles(videoTestElement: HTMLVideoElement): CodecProfile[] {
const CodecProfiles: CodecProfile[] = [];
const aacProfileConditions = getAacCodecProfileConditions(videoTestElement);
const supportsSecondaryAudio = isTizen();
if (aacProfileConditions.length > 0) {
CodecProfiles.push({
Type: CodecType.VideoAudio,
Codec: 'aac',
Conditions: aacProfileConditions
});
}
if (!supportsSecondaryAudio) {
CodecProfiles.push({
Type: CodecType.VideoAudio,
Conditions: [
createProfileCondition(
ProfileConditionValue.IsSecondaryAudio,
ProfileConditionType.Equals,
'false'
)
]
});
}
let maxH264Level = 42;
let h264Profiles = 'high|main|baseline|constrained baseline';
if (isTv() || videoTestElement.canPlayType('video/mp4; codecs="avc1.640833"').replace(/no/, '')) {
maxH264Level = 51;
}
if (videoTestElement.canPlayType('video/mp4; codecs="avc1.640834"').replace(/no/, '')) {
maxH264Level = 52;
}
if (
(isTizen() ||
videoTestElement.canPlayType('video/mp4; codecs="avc1.6e0033"').replace(/no/, '')) && // TODO: These tests are passing in Safari, but playback is failing
(!isApple() || !isWebOS() || !(isEdge() && !isChromiumBased()))
) {
h264Profiles += '|high 10';
}
let maxHevcLevel = 120;
let hevcProfiles = 'main';
const hevcProfilesMain10 = 'main|main 10';
// HEVC Main profile, Level 4.1
if (
videoTestElement.canPlayType('video/mp4; codecs="hvc1.1.4.L123"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hev1.1.4.L123"').replace(/no/, '')
) {
maxHevcLevel = 123;
}
// HEVC Main10 profile, Level 4.1
if (
videoTestElement.canPlayType('video/mp4; codecs="hvc1.2.4.L123"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hev1.2.4.L123"').replace(/no/, '')
) {
maxHevcLevel = 123;
hevcProfiles = hevcProfilesMain10;
}
// HEVC Main10 profile, Level 5.1
if (
videoTestElement.canPlayType('video/mp4; codecs="hvc1.2.4.L153"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hev1.2.4.L153"').replace(/no/, '')
) {
maxHevcLevel = 153;
hevcProfiles = hevcProfilesMain10;
}
// HEVC Main10 profile, Level 6.1
if (
videoTestElement.canPlayType('video/mp4; codecs="hvc1.2.4.L183"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hev1.2.4.L183"').replace(/no/, '')
) {
maxHevcLevel = 183;
hevcProfiles = hevcProfilesMain10;
}
const hevcCodecProfileConditions: ProfileCondition[] = [
createProfileCondition(
ProfileConditionValue.IsAnamorphic,
ProfileConditionType.NotEquals,
'true'
),
createProfileCondition(
ProfileConditionValue.VideoProfile,
ProfileConditionType.EqualsAny,
hevcProfiles
),
createProfileCondition(
ProfileConditionValue.VideoLevel,
ProfileConditionType.LessThanEqual,
maxHevcLevel.toString()
)
];
const h264CodecProfileConditions: ProfileCondition[] = [
createProfileCondition(
ProfileConditionValue.IsAnamorphic,
ProfileConditionType.NotEquals,
'true'
),
createProfileCondition(
ProfileConditionValue.VideoProfile,
ProfileConditionType.EqualsAny,
h264Profiles
),
createProfileCondition(
ProfileConditionValue.VideoLevel,
ProfileConditionType.LessThanEqual,
maxH264Level.toString()
)
];
if (!isTv()) {
h264CodecProfileConditions.push(
createProfileCondition(
ProfileConditionValue.IsInterlaced,
ProfileConditionType.NotEquals,
'true'
)
);
hevcCodecProfileConditions.push(
createProfileCondition(
ProfileConditionValue.IsInterlaced,
ProfileConditionType.NotEquals,
'true'
)
);
}
const globalMaxVideoBitrate = (getGlobalMaxVideoBitrate() || '').toString();
if (globalMaxVideoBitrate) {
h264CodecProfileConditions.push(
createProfileCondition(
ProfileConditionValue.VideoBitrate,
ProfileConditionType.LessThanEqual,
globalMaxVideoBitrate,
true
)
);
}
if (globalMaxVideoBitrate) {
hevcCodecProfileConditions.push(
createProfileCondition(
ProfileConditionValue.VideoBitrate,
ProfileConditionType.LessThanEqual,
globalMaxVideoBitrate,
true
)
);
}
// On iOS 12.x, for TS container max h264 level is 4.2
if (isApple() && isMobile() && Number(safariVersion()) < 13) {
const codecProfile = {
Type: CodecType.Video,
Codec: 'h264',
Container: 'ts',
Conditions: h264CodecProfileConditions.filter((condition) => {
return condition.Property !== 'VideoLevel';
})
};
codecProfile.Conditions.push(
createProfileCondition(
ProfileConditionValue.VideoLevel,
ProfileConditionType.LessThanEqual,
'42'
)
);
CodecProfiles.push(codecProfile);
}
CodecProfiles.push(
{
Type: CodecType.Video,
Codec: 'h264',
Conditions: h264CodecProfileConditions
},
{
Type: CodecType.Video,
Codec: 'hevc',
Conditions: hevcCodecProfileConditions
}
);
return CodecProfiles;
}

View File

@@ -0,0 +1,49 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import { getSupportedAudioCodecs } from './audio-formats';
import {
hasAacSupport,
hasAc3InHlsSupport,
hasAc3Support,
hasEac3Support,
hasMp3AudioSupport
} from './mp4-audio-formats';
import { isEdge } from '$lib/utils/browser-detection';
/**
* Gets an array with the supported fmp4 codecs
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns List of supported FMP4 audio codecs
*/
export function getSupportedFmp4AudioCodecs(videoTestElement: HTMLVideoElement): string[] {
const codecs = [];
if (hasAacSupport(videoTestElement)) {
codecs.push('aac');
}
if (hasMp3AudioSupport(videoTestElement)) {
codecs.push('mp3');
}
if (hasAc3Support(videoTestElement) && hasAc3InHlsSupport(videoTestElement)) {
codecs.push('ac3');
if (hasEac3Support(videoTestElement)) {
codecs.push('eac3');
}
}
if (getSupportedAudioCodecs('flac') && !isEdge()) {
codecs.push('flac');
}
if (getSupportedAudioCodecs('alac')) {
codecs.push('alac');
}
return codecs;
}

View File

@@ -0,0 +1,36 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import { hasH264Support, hasHevcSupport } from './mp4-video-formats';
import {
isApple,
isChrome,
isEdge,
isFirefox,
isTizen,
isWebOS
} from '$lib/utils/browser-detection';
/**
* Gets an array of supported fmp4 video codecs
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns List of supported fmp4 video codecs
*/
export function getSupportedFmp4VideoCodecs(videoTestElement: HTMLVideoElement): string[] {
const codecs = [];
if ((isApple() || isEdge() || isTizen() || isWebOS()) && hasHevcSupport(videoTestElement)) {
codecs.push('hevc');
}
if (
hasH264Support(videoTestElement) &&
(isChrome() || isFirefox() || isApple() || isEdge() || isTizen() || isWebOS())
) {
codecs.push('h264');
}
return codecs;
}

View File

@@ -0,0 +1,81 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import { hasH264Support, hasH265Support } from './mp4-video-formats';
import { hasEac3Support, hasAacSupport } from './mp4-audio-formats';
import { getSupportedAudioCodecs } from './audio-formats';
import { isTv } from '$lib/utils/browser-detection';
/**
* Check if client supports AC3 in HLS stream
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if the browser has AC3 in HLS support
*/
function supportsAc3InHls(videoTestElement: HTMLVideoElement): boolean | string {
if (isTv()) {
return true;
}
if (videoTestElement.canPlayType) {
return (
videoTestElement
.canPlayType('application/x-mpegurl; codecs="avc1.42E01E, ac-3"')
.replace(/no/, '') ||
videoTestElement
.canPlayType('application/vnd.apple.mpegURL; codecs="avc1.42E01E, ac-3"')
.replace(/no/, '')
);
}
return false;
}
/**
* Gets the supported HLS video codecs
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Array of video codecs supported in HLS
*/
export function getHlsVideoCodecs(videoTestElement: HTMLVideoElement): string[] {
const hlsVideoCodecs = [];
if (hasH264Support(videoTestElement)) {
hlsVideoCodecs.push('h264');
}
if (hasH265Support(videoTestElement) || isTv()) {
hlsVideoCodecs.push('h265', 'hevc');
}
return hlsVideoCodecs;
}
/**
* Gets the supported HLS audio codecs
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Array of audio codecs supported in HLS
*/
export function getHlsAudioCodecs(videoTestElement: HTMLVideoElement): string[] {
const hlsVideoAudioCodecs = [];
if (supportsAc3InHls(videoTestElement)) {
hlsVideoAudioCodecs.push('ac3');
if (hasEac3Support(videoTestElement)) {
hlsVideoAudioCodecs.push('eac3');
}
}
if (hasAacSupport(videoTestElement)) {
hlsVideoAudioCodecs.push('aac');
}
if (getSupportedAudioCodecs('opus')) {
hlsVideoAudioCodecs.push('opus');
}
return hlsVideoAudioCodecs;
}

View File

@@ -0,0 +1,180 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import { hasVp8Support } from './mp4-video-formats';
import { getSupportedAudioCodecs } from './audio-formats';
import {
isTizen,
isTizen4,
isTizen5,
isTizen55,
isTv,
isWebOS
} from '$lib/utils/browser-detection';
/**
* Checks if the client can play the AC3 codec
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if the browser has AC3 support
*/
export function hasAc3Support(videoTestElement: HTMLVideoElement): boolean {
if (isTv()) {
return true;
}
return !!videoTestElement.canPlayType('audio/mp4; codecs="ac-3"').replace(/no/, '');
}
/**
* Checks if the client can play AC3 in a HLS stream
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if the browser has AC3 support
*/
export function hasAc3InHlsSupport(videoTestElement: HTMLVideoElement): boolean {
if (isTizen() || isWebOS()) {
return true;
}
if (videoTestElement.canPlayType) {
return !!(
videoTestElement
.canPlayType('application/x-mpegurl; codecs="avc1.42E01E, ac-3"')
.replace(/no/, '') ||
videoTestElement
.canPlayType('application/vnd.apple.mpegURL; codecs="avc1.42E01E, ac-3"')
.replace(/no/, '')
);
}
return false;
}
/**
* Checks if the cliemt has E-AC3 codec support
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if browser has EAC3 support
*/
export function hasEac3Support(videoTestElement: HTMLVideoElement): boolean {
if (isTv()) {
return true;
}
return !!videoTestElement.canPlayType('audio/mp4; codecs="ec-3"').replace(/no/, '');
}
/**
* Checks if the client has AAC codec support
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if browser has AAC support
*/
export function hasAacSupport(videoTestElement: HTMLVideoElement): boolean {
return !!videoTestElement
.canPlayType('video/mp4; codecs="avc1.640029, mp4a.40.2"')
.replace(/no/, '');
}
/**
* Checks if the client has MP2 codec support
*
* @returns Determines if browser has MP2 support
*/
export function hasMp2AudioSupport(): boolean {
return isTv();
}
/**
* Checks if the client has MP3 audio codec support
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if browser has Mp3 support
*/
export function hasMp3AudioSupport(videoTestElement: HTMLVideoElement): boolean {
return !!(
videoTestElement.canPlayType('video/mp4; codecs="avc1.640029, mp4a.69"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="avc1.640029, mp4a.6B"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="avc1.640029, mp3"').replace(/no/, '')
);
}
/**
* Determines DTS audio support
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if browserr has DTS audio support
*/
export function hasDtsSupport(videoTestElement: HTMLVideoElement): boolean | string {
// DTS audio not supported in 2018 models (Tizen 4.0)
if (isTizen4() || isTizen5() || isTizen55()) {
return false;
}
return (
isTv() ||
videoTestElement.canPlayType('video/mp4; codecs="dts-"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="dts+"').replace(/no/, '')
);
}
/**
* Gets an array of supported MP4 codecs
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Array of supported MP4 audio codecs
*/
export function getSupportedMP4AudioCodecs(videoTestElement: HTMLVideoElement): string[] {
const codecs = [];
if (hasAacSupport(videoTestElement)) {
codecs.push('aac');
}
if (hasMp3AudioSupport(videoTestElement)) {
codecs.push('mp3');
}
if (hasAc3Support(videoTestElement)) {
codecs.push('ac3');
if (hasEac3Support(videoTestElement)) {
codecs.push('eac3');
}
}
if (hasMp2AudioSupport()) {
codecs.push('mp2');
}
if (hasDtsSupport(videoTestElement)) {
codecs.push('dca', 'dts');
}
if (isTizen() || isWebOS()) {
codecs.push('pcm_s16le', 'pcm_s24le');
}
if (isTizen()) {
codecs.push('aac_latm');
}
if (getSupportedAudioCodecs('opus')) {
codecs.push('opus');
}
if (getSupportedAudioCodecs('flac')) {
codecs.push('flac');
}
if (getSupportedAudioCodecs('alac')) {
codecs.push('alac');
}
if (hasVp8Support(videoTestElement) || isTizen()) {
codecs.push('vorbis');
}
return codecs;
}

View File

@@ -0,0 +1,158 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import { isApple, isTizen, isTizen55, isTv, isWebOS5 } from '$lib/utils/browser-detection';
/**
* Checks if the client has support for the H264 codec
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if browser has H264 support
*/
export function hasH264Support(videoTestElement: HTMLVideoElement): boolean {
return !!(
videoTestElement.canPlayType &&
videoTestElement.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"').replace(/no/, '')
);
}
/**
* Checks if the client has support for the H265 codec
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if browser has H265 support
*/
export function hasH265Support(videoTestElement: HTMLVideoElement): boolean {
if (isTv()) {
return true;
}
return !!(
videoTestElement.canPlayType &&
(videoTestElement.canPlayType('video/mp4; codecs="hvc1.1.L120"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hev1.1.L120"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hvc1.1.0.L120"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hev1.1.0.L120"').replace(/no/, ''))
);
}
/**
* Checks if the client has support for the HEVC codec
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if browser has HEVC Support
*/
export function hasHevcSupport(videoTestElement: HTMLVideoElement): boolean {
if (isTv()) {
return true;
}
return !!(
!!videoTestElement.canPlayType &&
(videoTestElement.canPlayType('video/mp4; codecs="hvc1.1.L120"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hev1.1.L120"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hvc1.1.0.L120"').replace(/no/, '') ||
videoTestElement.canPlayType('video/mp4; codecs="hev1.1.0.L120"').replace(/no/, ''))
);
}
/**
* Checks if the client has support for the AV1 codec
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if browser has AV1 support
*/
export function hasAv1Support(videoTestElement: HTMLVideoElement): boolean {
if ((isTizen() && isTizen55()) || (isWebOS5() && window.outerHeight >= 2160)) {
return true;
}
return !!(
videoTestElement.canPlayType &&
videoTestElement.canPlayType('video/webm; codecs="av01.0.15M.10"').replace(/no/, '')
);
}
/**
* Check if the client has support for the VC1 codec
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if browser has VC1 support
*/
function hasVc1Support(videoTestElement: HTMLVideoElement): boolean {
return !!(isTv() || videoTestElement.canPlayType('video/mp4; codecs="vc-1"').replace(/no/, ''));
}
/**
* Checks if the client has support for the VP8 codec
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if browser has VP8 support
*/
export function hasVp8Support(videoTestElement: HTMLVideoElement): boolean {
return !!(
videoTestElement.canPlayType &&
videoTestElement.canPlayType('video/webm; codecs="vp8"').replace(/no/, '')
);
}
/**
* Checks if the client has support for the VP9 codec
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if browser has VP9 support
*/
export function hasVp9Support(videoTestElement: HTMLVideoElement): boolean {
return !!(
videoTestElement.canPlayType &&
videoTestElement.canPlayType('video/webm; codecs="vp9"').replace(/no/, '')
);
}
/**
* Queries the platform for the codecs suppers in an MP4 container.
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Array of codec identifiers.
*/
export function getSupportedMP4VideoCodecs(videoTestElement: HTMLVideoElement): string[] {
const codecs = [];
if (hasH264Support(videoTestElement)) {
codecs.push('h264');
}
if (
hasHevcSupport(videoTestElement) && // Safari is lying on HDR and 60fps videos, use fMP4 instead
!isApple()
) {
codecs.push('hevc');
}
if (isTv()) {
codecs.push('mpeg2video');
}
if (hasVc1Support(videoTestElement)) {
codecs.push('vc1');
}
if (isTizen()) {
codecs.push('msmpeg4v2');
}
if (hasVp8Support(videoTestElement)) {
codecs.push('vp8');
}
if (hasVp9Support(videoTestElement)) {
codecs.push('vp9');
}
if (hasAv1Support(videoTestElement)) {
codecs.push('av1');
}
return codecs;
}

View File

@@ -0,0 +1,47 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import { isEdge, isTizen, isTv, supportsMediaSource } from '$lib/utils/browser-detection';
/**
* Checks if the client can play native HLS
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns Determines if the browser can play native Hls
*/
export function canPlayNativeHls(videoTestElement: HTMLVideoElement): boolean {
if (isTizen()) {
return true;
}
return !!(
videoTestElement.canPlayType('application/x-mpegURL').replace(/no/, '') ||
videoTestElement.canPlayType('application/vnd.apple.mpegURL').replace(/no/, '')
);
}
/**
* Determines if the browser can play Hls with Media Source Extensions
*/
export function canPlayHlsWithMSE(): boolean {
return supportsMediaSource();
}
/**
* Determines if the browser can play Mkvs
*/
export function hasMkvSupport(videoTestElement: HTMLVideoElement): boolean {
if (isTv()) {
return true;
}
if (
videoTestElement.canPlayType('video/x-matroska').replace(/no/, '') ||
videoTestElement.canPlayType('video/mkv').replace(/no/, '')
) {
return true;
}
return !!isEdge();
}

View File

@@ -0,0 +1,38 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import {
hasAacSupport,
hasAc3InHlsSupport,
hasAc3Support,
hasEac3Support,
hasMp3AudioSupport
} from './mp4-audio-formats';
/**
* List of supported Ts audio codecs
*/
export function getSupportedTsAudioCodecs(
videoTestElement: HTMLVideoElement
): string[] {
const codecs = [];
if (hasAacSupport(videoTestElement)) {
codecs.push('aac');
}
if (hasMp3AudioSupport(videoTestElement)) {
codecs.push('mp3');
}
if (hasAc3Support(videoTestElement) && hasAc3InHlsSupport(videoTestElement)) {
codecs.push('ac3');
if (hasEac3Support(videoTestElement)) {
codecs.push('eac3');
}
}
return codecs;
}

View File

@@ -0,0 +1,20 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import { hasH264Support } from './mp4-video-formats';
/**
* List of supported ts video codecs
*/
export function getSupportedTsVideoCodecs(
videoTestElement: HTMLVideoElement
): string[] {
const codecs = [];
if (hasH264Support(videoTestElement)) {
codecs.push('h264');
}
return codecs;
}

View File

@@ -0,0 +1,20 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import { isWebOS } from '$lib/utils/browser-detection';
/**
* Get an array of supported codecs
*/
export function getSupportedWebMAudioCodecs(videoTestElement: HTMLVideoElement): string[] {
const codecs = [];
codecs.push('vorbis');
if (!isWebOS() && videoTestElement.canPlayType('audio/ogg; codecs="opus"').replace(/no/, '')) {
codecs.push('opus');
}
return codecs;
}

View File

@@ -0,0 +1,32 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import {
hasAv1Support,
hasVp8Support,
hasVp9Support
} from './mp4-video-formats';
/**
* Get an array of supported codecs WebM video codecs
*/
export function getSupportedWebMVideoCodecs(
videoTestElement: HTMLVideoElement
): string[] {
const codecs = [];
if (hasVp8Support(videoTestElement)) {
codecs.push('vp8');
}
if (hasVp9Support(videoTestElement)) {
codecs.push('vp9');
}
if (hasAv1Support(videoTestElement)) {
codecs.push('av1');
}
return codecs;
}

View File

@@ -0,0 +1,58 @@
/**
* @deprecated
* Since we're targeting modern environments/devices only, it makes sense to switch
* to the native MediaCapabilities API, widely supported on modern devices, but not in older.
*
* Given a media file, we should test with MC the compatibility of video, audio and subtitle streams
* independently:
* If success: Don't request transcoding and direct play that specific stream.
* If failure: Request transcoding of the failing streams to a previously hardcoded
* bitrate/codec combination
*
* For the hardcoded bitrate/codecs combination we can use what we know that are universally
* compatible, even without testing for explicit compatibility (we can do simple checks,
* but the more we do, the complex/less portable our solution can get).
* Examples: H264, AAC and VTT/SASS (thanks to JASSUB).
*
* Other codec combinations can be hardcoded, even if they're not direct-playable in
* most browsers (like H265 or AV1), so the few browsers that support them benefits from less bandwidth
* usage (although this will rarely happen: The most expected situations when transcoding
* is when the media's codecs are more "powerful" than what the client is capable of, and H265 is
* pretty modern, so it would've been catched-up by MediaCapabilities. However,
* we must take into account the playback of really old codecs like MPEG or H263,
* whose support are probably likely going to be removed from browsers,
* so MediaCapabilities reports as unsupported, so we would be going from an "inferior" codec to a
* "superior" codec in this situation)
*/
import type { DeviceProfile as DP } from '@jellyfin/sdk/lib/generated-client';
import { getCodecProfiles } from './helpers/codec-profiles';
import { getDirectPlayProfiles } from './directplay-profile';
import { getTranscodingProfiles } from './transcoding-profile';
import { getSubtitleProfiles } from './subtitle-profile';
import { getResponseProfiles } from './response-profile';
export type DeviceProfile = DP;
/**
* Creates a device profile containing supported codecs for the active Cast device.
*
* @param videoTestElement - Dummy video element for compatibility tests
* @returns Device profile.
*/
function getDeviceProfile(videoTestElement?: HTMLVideoElement): DP {
const element = videoTestElement || document.createElement('video');
return {
MaxStreamingBitrate: 120_000_000,
MaxStaticBitrate: 0,
MusicStreamingTranscodingBitrate: Math.min(120_000_000, 192_000),
DirectPlayProfiles: getDirectPlayProfiles(element),
TranscodingProfiles: getTranscodingProfiles(element),
ContainerProfiles: [],
CodecProfiles: getCodecProfiles(element),
SubtitleProfiles: getSubtitleProfiles(),
ResponseProfiles: getResponseProfiles()
};
}
export default getDeviceProfile;

View File

@@ -0,0 +1,23 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import { DlnaProfileType } from '@jellyfin/sdk/lib/generated-client';
import type { ResponseProfile } from '@jellyfin/sdk/lib/generated-client';
/**
* Returns a valid ResponseProfile for the current platform.
*
* @returns An array of subtitle profiles for the current platform.
*/
export function getResponseProfiles(): Array<ResponseProfile> {
const ResponseProfiles = [];
ResponseProfiles.push({
Type: DlnaProfileType.Video,
Container: 'm4v',
MimeType: 'video/mp4'
});
return ResponseProfiles;
}

View File

@@ -0,0 +1,32 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import { SubtitleDeliveryMethod } from '@jellyfin/sdk/lib/generated-client';
import type { SubtitleProfile } from '@jellyfin/sdk/lib/generated-client';
/**
* Returns a valid SubtitleProfile for the current platform.
*
* @returns An array of subtitle profiles for the current platform.
*/
export function getSubtitleProfiles(): Array<SubtitleProfile> {
const SubtitleProfiles: Array<SubtitleProfile> = [];
SubtitleProfiles.push(
{
Format: 'vtt',
Method: SubtitleDeliveryMethod.External
},
{
Format: 'ass',
Method: SubtitleDeliveryMethod.External
},
{
Format: 'ssa',
Method: SubtitleDeliveryMethod.External
}
);
return SubtitleProfiles;
}

View File

@@ -0,0 +1,118 @@
/**
* @deprecated - Check @/utils/playback-profiles/index
*/
import { DlnaProfileType, EncodingContext } from '@jellyfin/sdk/lib/generated-client';
import type { TranscodingProfile } from '@jellyfin/sdk/lib/generated-client';
import { getSupportedAudioCodecs } from './helpers/audio-formats';
import { getSupportedMP4AudioCodecs } from './helpers/mp4-audio-formats';
import { getSupportedMP4VideoCodecs, hasVp8Support } from './helpers/mp4-video-formats';
import { canPlayNativeHls, canPlayHlsWithMSE, hasMkvSupport } from './helpers/transcoding-formats';
import { getSupportedTsAudioCodecs } from './helpers/ts-audio-formats';
import { getSupportedTsVideoCodecs } from './helpers/ts-video-formats';
import {
isTv,
isApple,
isEdge,
isChromiumBased,
isAndroid,
isTizen
} from '$lib/utils/browser-detection';
/**
* Returns a valid TranscodingProfile for the current platform.
*
* @param videoTestElement - A HTML video element for testing codecs
* @returns An array of transcoding profiles for the current platform.
*/
export function getTranscodingProfiles(
videoTestElement: HTMLVideoElement
): Array<TranscodingProfile> {
const TranscodingProfiles: TranscodingProfile[] = [];
const physicalAudioChannels = isTv() ? 6 : 2;
const hlsBreakOnNonKeyFrames = !!(
isApple() ||
(isEdge() && !isChromiumBased()) ||
!canPlayNativeHls(videoTestElement)
);
const mp4AudioCodecs = getSupportedMP4AudioCodecs(videoTestElement);
const mp4VideoCodecs = getSupportedMP4VideoCodecs(videoTestElement);
const canPlayHls = canPlayNativeHls(videoTestElement) || canPlayHlsWithMSE();
if (canPlayHls) {
TranscodingProfiles.push({
// hlsjs, edge, and android all seem to require ts container
Container:
!canPlayNativeHls(videoTestElement) || (isEdge() && !isChromiumBased()) || isAndroid()
? 'ts'
: 'aac',
Type: DlnaProfileType.Audio,
AudioCodec: 'aac',
Context: EncodingContext.Streaming,
Protocol: 'hls',
MaxAudioChannels: physicalAudioChannels.toString(),
MinSegments: isApple() ? 2 : 1,
BreakOnNonKeyFrames: hlsBreakOnNonKeyFrames
});
}
for (const audioFormat of ['aac', 'mp3', 'opus', 'wav'].filter((format) =>
getSupportedAudioCodecs(format)
)) {
TranscodingProfiles.push({
Container: audioFormat,
Type: DlnaProfileType.Audio,
AudioCodec: audioFormat,
Context: EncodingContext.Streaming,
Protocol: 'http',
MaxAudioChannels: physicalAudioChannels.toString()
});
}
const hlsInTsVideoCodecs = getSupportedTsVideoCodecs(videoTestElement);
const hlsInTsAudioCodecs = getSupportedTsAudioCodecs(videoTestElement);
if (canPlayHls && hlsInTsVideoCodecs.length > 0 && hlsInTsAudioCodecs.length > 0) {
TranscodingProfiles.push({
Container: 'ts',
Type: DlnaProfileType.Video,
AudioCodec: hlsInTsAudioCodecs.join(','),
VideoCodec: hlsInTsVideoCodecs.join(','),
Context: EncodingContext.Streaming,
Protocol: 'hls',
MaxAudioChannels: physicalAudioChannels.toString(),
MinSegments: isApple() ? 2 : 1,
BreakOnNonKeyFrames: hlsBreakOnNonKeyFrames
});
}
if (hasMkvSupport(videoTestElement) && !isTizen()) {
TranscodingProfiles.push({
Container: 'mkv',
Type: DlnaProfileType.Video,
AudioCodec: mp4AudioCodecs.join(','),
VideoCodec: mp4VideoCodecs.join(','),
Context: EncodingContext.Streaming,
MaxAudioChannels: physicalAudioChannels.toString(),
CopyTimestamps: true
});
}
if (hasVp8Support(videoTestElement)) {
TranscodingProfiles.push({
Container: 'webm',
Type: DlnaProfileType.Video,
AudioCodec: 'vorbis',
VideoCodec: 'vpx',
Context: EncodingContext.Streaming,
Protocol: 'http',
// If audio transcoding is needed, limit channels to number of physical audio channels
// Trying to transcode to 5 channels when there are only 2 speakers generally does not sound good
MaxAudioChannels: physicalAudioChannels.toString()
});
}
return TranscodingProfiles;
}

5289
src/lib/apis/radarr/radarr.generated.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,144 @@
import createClient from 'openapi-fetch';
import { log, request } from '$lib/utils';
import type { paths } from '$lib/apis/radarr/radarr.generated';
import type { components } from '$lib/apis/radarr/radarr.generated';
import { fetchTmdbMovie } from '$lib/apis/tmdbApi';
import { PUBLIC_RADARR_API_KEY, PUBLIC_RADARR_BASE_URL } from '$env/static/public';
export type RadarrMovie = components['schemas']['MovieResource'];
export type MovieFileResource = components['schemas']['MovieFileResource'];
export type ReleaseResource = components['schemas']['ReleaseResource'];
export type RadarrDownload = components['schemas']['QueueResource'] & { movie: RadarrMovie };
export interface RadarrMovieOptions {
title: string;
qualityProfileId: number;
minimumAvailability: 'announced' | 'inCinemas' | 'released';
tags: number[];
profileId: number;
year: number;
rootFolderPath: string;
tmdbId: number;
monitored?: boolean;
searchNow?: boolean;
}
export const RadarrApi = createClient<paths>({
baseUrl: PUBLIC_RADARR_BASE_URL,
headers: {
'X-Api-Key': PUBLIC_RADARR_API_KEY
}
});
export const getRadarrMovies = (): Promise<RadarrMovie[]> =>
RadarrApi.get('/api/v3/movie', {
params: {}
}).then((r) => r.data || []);
export const requestRadarrMovie = () => request(getRadarrMovie);
export const getRadarrMovie = (tmdbId: string): Promise<RadarrMovie | undefined> =>
RadarrApi.get('/api/v3/movie', {
params: {
query: {
tmdbId: Number(tmdbId)
}
}
}).then((r) => r.data?.find((m) => (m.tmdbId as any) == tmdbId));
export const requestAddRadarrMovie = () => request(addRadarrMovie);
export const addRadarrMovie = async (tmdbId: string) => {
const tmdbMovie = await fetchTmdbMovie(tmdbId);
const radarrMovie = await getMovieByTmdbIdByTmdbId(tmdbId);
console.log('fetched movies', tmdbMovie, radarrMovie);
if (radarrMovie?.id) throw new Error('Movie already exists');
if (!tmdbMovie) throw new Error('Movie not found');
const qualityProfile = 4;
const options: RadarrMovieOptions = {
qualityProfileId: qualityProfile,
profileId: qualityProfile,
rootFolderPath: '/movies',
minimumAvailability: 'announced',
title: tmdbMovie.title,
tmdbId: tmdbMovie.id,
year: Number((await tmdbMovie).release_date.slice(0, 4)),
monitored: false,
tags: [],
searchNow: false
};
return RadarrApi.post('/api/v3/movie', {
params: {},
body: options
}).then((r) => r.data);
};
export const cancelDownloadRadarrMovie = async (downloadId: number) => {
const deleteResponse = await RadarrApi.del('/api/v3/queue/{id}', {
params: {
path: {
id: downloadId
},
query: {
blocklist: false,
removeFromClient: true
}
}
}).then((r) => log(r));
return deleteResponse.response.ok;
};
export const requestRadarrReleases = () => request(fetchRadarrReleases);
export const fetchRadarrReleases = (movieId: string) =>
RadarrApi.get('/api/v3/release', { params: { query: { movieId: Number(movieId) } } }).then(
(r) => r.data
);
export const requestDownloadRadarrMovie = () => request(downloadRadarrMovie);
export const downloadRadarrMovie = (guid: string) =>
RadarrApi.post('/api/v3/release', {
params: {},
body: {
indexerId: 2,
guid
}
});
export const deleteRadarrMovie = (id: number) =>
RadarrApi.del('/api/v3/moviefile/{id}', {
params: {
path: {
id
}
}
}).then((res) => res.response.ok);
export const requestRadarrQueuedById = () => request(getRadarrDownload);
export const getRadarrDownloads = (): Promise<RadarrDownload[]> =>
RadarrApi.get('/api/v3/queue', {
params: {
query: {
includeMovie: true
}
}
}).then((r) => (r.data?.records?.filter((record) => record.movie) as RadarrDownload[]) || []);
export const getRadarrDownload = (id: string) =>
getRadarrDownloads().then((downloads) => downloads.find((d) => d.movie.id === Number(id)));
const getMovieByTmdbIdByTmdbId = (tmdbId: string) =>
RadarrApi.get('/api/v3/movie/lookup/tmdb', {
params: {
query: {
tmdbId: Number(tmdbId)
}
}
}).then((r) => r.data as any as RadarrMovie);

5087
src/lib/apis/sonarr/sonarr.generated.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
import createClient from 'openapi-fetch';
import type { paths } from '$lib/apis/sonarr/sonarr.generated';
import { PUBLIC_SONARR_API_KEY, PUBLIC_SONARR_BASE_URL } from '$env/static/public';
export const SonarrApi = createClient<paths>({
baseUrl: PUBLIC_SONARR_BASE_URL,
headers: {
'X-Api-Key': PUBLIC_SONARR_API_KEY
}
});

229
src/lib/apis/tmdbApi.ts Normal file
View File

@@ -0,0 +1,229 @@
import axios from 'axios';
import { PUBLIC_TMDB_API_KEY } from '$env/static/public';
import { request } from '$lib/utils';
export const TmdbApi = axios.create({
baseURL: 'https://api.themoviedb.org/3',
headers: {
Authorization: `Bearer ${PUBLIC_TMDB_API_KEY}`
}
});
export const fetchTmdbMovie = async (tmdbId: string): Promise<TmdbMovie> =>
await TmdbApi.get<TmdbMovie>('/movie/' + tmdbId).then((r) => r.data);
export const fetchTmdbMovieVideos = async (tmdbId: string): Promise<Video[]> =>
await TmdbApi.get<VideosResponse>('/movie/' + tmdbId + '/videos').then((res) => res.data.results);
export const fetchTmdbMovieImages = async (tmdbId: string): Promise<ImagesResponse> =>
await TmdbApi.get<ImagesResponse>('/movie/' + tmdbId + '/images').then((res) => res.data);
export const fetchTmdbMovieCredits = async (tmdbId: string): Promise<CastMember[]> =>
await TmdbApi.get<CreditsResponse>('/movie/' + tmdbId + '/credits').then((res) => res.data.cast);
export const fetchTmdbPopularMovies = () =>
TmdbApi.get<PopularMoviesResponse>('/movie/popular').then((res) => res.data.results);
export const requestTmdbPopularMovies = () => request(fetchTmdbPopularMovies, null);
export interface TmdbMovieFull extends TmdbMovie {
videos: Video[];
images: {
backdrops: Backdrop[];
logos: Logo[];
posters: Poster[];
};
credits: CastMember[];
}
export type MovieDetailsResponse = TmdbMovie;
export interface TmdbMovie {
adult: boolean;
backdrop_path: string;
belongs_to_collection: any;
budget: number;
genres: Genre[];
homepage: string;
id: number;
imdb_id: string;
original_language: string;
original_title: string;
overview: string;
popularity: number;
poster_path: string;
production_companies: ProductionCompany[];
production_countries: ProductionCountry[];
release_date: string;
revenue: number;
runtime: number;
spoken_languages: SpokenLanguage[];
status: string;
tagline: string;
title: string;
video: boolean;
vote_average: number;
vote_count: number;
}
export interface Genre {
id: number;
name: string;
}
export interface ProductionCompany {
id: number;
logo_path?: string;
name: string;
origin_country: string;
}
export interface ProductionCountry {
iso_3166_1: string;
name: string;
}
export interface SpokenLanguage {
english_name: string;
iso_639_1: string;
name: string;
}
export interface CreditsResponse {
id: number;
cast: CastMember[];
crew: CrewMember[];
}
export interface CastMember {
adult: boolean;
gender: number;
id: number;
known_for_department: string;
name: string;
original_name: string;
popularity: number;
profile_path?: string;
cast_id: number;
character: string;
credit_id: string;
order: number;
}
export interface CrewMember {
adult: boolean;
gender: number;
id: number;
known_for_department: string;
name: string;
original_name: string;
popularity: number;
profile_path?: string;
credit_id: string;
department: string;
job: string;
}
export interface VideosResponse {
id: number;
results: Video[];
}
export interface Video {
iso_639_1: string;
iso_3166_1: string;
name: string;
key: string;
site: string;
size: number;
type: string;
official: boolean;
published_at: string;
id: string;
}
export interface ImagesResponse {
backdrops: Backdrop[];
id: number;
logos: Logo[];
posters: Poster[];
}
export interface Backdrop {
aspect_ratio: number;
height: number;
iso_639_1?: string;
file_path: string;
vote_average: number;
vote_count: number;
width: number;
}
export interface Logo {
aspect_ratio: number;
height: number;
iso_639_1: string;
file_path: string;
vote_average: number;
vote_count: number;
width: number;
}
export interface Poster {
aspect_ratio: number;
height: number;
iso_639_1?: string;
file_path: string;
vote_average: number;
vote_count: number;
width: number;
}
export interface MultiSearchResponse {
page: number;
results: MultiSearchResult[];
total_pages: number;
total_results: number;
}
export interface MultiSearchResult {
adult: boolean;
backdrop_path?: string;
id: number;
title: string;
original_language: string;
original_title: string;
overview: string;
poster_path?: string;
media_type: string;
genre_ids: number[];
popularity: number;
release_date: string;
video: boolean;
vote_average: number;
vote_count: number;
}
export interface PopularMoviesResponse {
page: number;
results: PopularMovieResult[];
total_pages: number;
total_results: number;
}
export interface PopularMovieResult {
adult: boolean;
backdrop_path: string;
genre_ids: number[];
id: number;
original_language: string;
original_title: string;
overview: string;
popularity: number;
poster_path: string;
release_date: string;
title: string;
video: boolean;
vote_average: number;
vote_count: number;
}