Work on the movie page
This commit is contained in:
54
backend/package-lock.json
generated
54
backend/package-lock.json
generated
@@ -16,6 +16,8 @@
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@types/express-http-proxy": "^1.6.6",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
@@ -2251,7 +2253,6 @@
|
||||
"version": "1.19.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
|
||||
"integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/connect": "*",
|
||||
"@types/node": "*"
|
||||
@@ -2261,7 +2262,6 @@
|
||||
"version": "3.4.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
|
||||
"integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
@@ -2302,7 +2302,6 @@
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
|
||||
"integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/body-parser": "*",
|
||||
"@types/express-serve-static-core": "^4.17.33",
|
||||
@@ -2310,11 +2309,18 @@
|
||||
"@types/serve-static": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express-http-proxy": {
|
||||
"version": "1.6.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-http-proxy/-/express-http-proxy-1.6.6.tgz",
|
||||
"integrity": "sha512-J8ZqHG76rq1UB716IZ3RCmUhg406pbWxsM3oFCFccl5xlWUPzoR4if6Og/cE4juK8emH0H9quZa5ltn6ZdmQJg==",
|
||||
"dependencies": {
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/express-serve-static-core": {
|
||||
"version": "4.17.43",
|
||||
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz",
|
||||
"integrity": "sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/qs": "*",
|
||||
@@ -2334,8 +2340,7 @@
|
||||
"node_modules/@types/http-errors": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
|
||||
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA=="
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
@@ -2394,8 +2399,7 @@
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
|
||||
"dev": true
|
||||
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.11.30",
|
||||
@@ -2408,14 +2412,12 @@
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz",
|
||||
"integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA=="
|
||||
},
|
||||
"node_modules/@types/range-parser": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.8",
|
||||
@@ -2427,7 +2429,6 @@
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
|
||||
"integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/mime": "^1",
|
||||
"@types/node": "*"
|
||||
@@ -2437,7 +2438,6 @@
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz",
|
||||
"integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/http-errors": "*",
|
||||
"@types/mime": "*",
|
||||
@@ -4527,6 +4527,11 @@
|
||||
"integrity": "sha512-7nOqkomXZEaxUDJw21XZNtRk739QvrPSoZoRtbsEfcii00vdzZUh6zh1CQwHhrib8MdEtJfv5rJiGeb4KuV/vw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
|
||||
@@ -4925,6 +4930,27 @@
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-http-proxy": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/express-http-proxy/-/express-http-proxy-2.0.0.tgz",
|
||||
"integrity": "sha512-TXxcPFTWVUMSEmyM6iX2sT/JtmqhqngTq29P+eXTVFdtxZrTmM8THUYK59rUXiln0FfPGvxEpGRnVrgvHksXDw==",
|
||||
"dependencies": {
|
||||
"debug": "^3.0.1",
|
||||
"es6-promise": "^4.1.1",
|
||||
"raw-body": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express-http-proxy/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/serve-static": "^4.0.1",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@types/express-http-proxy": "^1.6.6",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"reflect-metadata": "^0.2.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"sqlite3": "^5.1.7",
|
||||
|
||||
@@ -3,11 +3,15 @@ import { AppModule } from './app.module';
|
||||
import 'reflect-metadata';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import * as fs from 'fs';
|
||||
import * as proxy from 'express-http-proxy';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.setGlobalPrefix('api');
|
||||
app.enableCors();
|
||||
|
||||
// app.use('/api/proxy/jellyfin', proxy('http://192.168.0.129:8096'));
|
||||
|
||||
const config = new DocumentBuilder().build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config, {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import axios from 'axios';
|
||||
import createClient from 'openapi-fetch';
|
||||
import { get } from 'svelte/store';
|
||||
import type { components, paths } from './jellyfin.generated';
|
||||
import { settings } from '../../stores/settings.store';
|
||||
import type { DeviceProfile } from './playback-profiles';
|
||||
import type { Api } from '../api.interface';
|
||||
import { appState } from '../../stores/app-state.store';
|
||||
import type { DeviceProfile } from './playback-profiles';
|
||||
|
||||
export type JellyfinItem = components['schemas']['BaseItemDto'];
|
||||
|
||||
@@ -29,6 +27,10 @@ export class JellyfinApi implements Api<paths> {
|
||||
return get(appState).user?.settings.jellyfin.userId || '';
|
||||
}
|
||||
|
||||
getApiKey() {
|
||||
return get(appState).user?.settings.jellyfin.apiKey || '';
|
||||
}
|
||||
|
||||
async getContinueWatching(): Promise<JellyfinItem[] | undefined> {
|
||||
return this.getClient()
|
||||
.GET('/Users/{userId}/Items/Resume', {
|
||||
@@ -45,24 +47,28 @@ export class JellyfinApi implements Api<paths> {
|
||||
.then((r) => r.data?.Items || []);
|
||||
}
|
||||
|
||||
async getLibraryItems() {
|
||||
return (
|
||||
this.getClient()
|
||||
.GET('/Users/{userId}/Items', {
|
||||
params: {
|
||||
path: {
|
||||
userId: this.getUserId()
|
||||
},
|
||||
query: {
|
||||
hasTmdbId: true,
|
||||
recursive: true,
|
||||
includeItemTypes: ['Movie', 'Series'],
|
||||
fields: ['ProviderIds', 'Genres', 'DateLastMediaAdded', 'DateCreated']
|
||||
jellyfinItemsCache: JellyfinItem[] = [];
|
||||
async getLibraryItems(refreshCache = false) {
|
||||
if (refreshCache || !this.jellyfinItemsCache.length) {
|
||||
this.jellyfinItemsCache =
|
||||
(await this.getClient()
|
||||
.GET('/Users/{userId}/Items', {
|
||||
params: {
|
||||
path: {
|
||||
userId: this.getUserId()
|
||||
},
|
||||
query: {
|
||||
hasTmdbId: true,
|
||||
recursive: true,
|
||||
includeItemTypes: ['Movie', 'Series'],
|
||||
fields: ['ProviderIds', 'Genres', 'DateLastMediaAdded', 'DateCreated']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => r.data?.Items || []) || Promise.resolve([])
|
||||
);
|
||||
})
|
||||
.then((r) => r.data?.Items || [])) || Promise.resolve([]);
|
||||
}
|
||||
|
||||
return this.jellyfinItemsCache;
|
||||
}
|
||||
|
||||
getPosterUrl(item: JellyfinItem, quality = 100, original = false) {
|
||||
@@ -74,6 +80,113 @@ export class JellyfinApi implements Api<paths> {
|
||||
}`
|
||||
: '';
|
||||
}
|
||||
|
||||
getLibraryItem(itemId: string, refreshCache = false) {
|
||||
return this.getLibraryItems(refreshCache).then((items) => items.find((i) => i.Id === itemId));
|
||||
}
|
||||
|
||||
getLibraryItemFromTmdbId(tmdbId: string, refreshCache = false) {
|
||||
return this.getLibraryItems(refreshCache).then((items) =>
|
||||
items.find((i) => i.ProviderIds?.Tmdb === tmdbId)
|
||||
);
|
||||
}
|
||||
|
||||
// PLAYBACK
|
||||
|
||||
getPlaybackInfo = async (
|
||||
itemId: string,
|
||||
playbackProfile: DeviceProfile,
|
||||
startTimeTicks = 0,
|
||||
maxStreamingBitrate = 140000000
|
||||
) =>
|
||||
this.getClient()
|
||||
?.POST('/Items/{itemId}/PlaybackInfo', {
|
||||
params: {
|
||||
path: {
|
||||
itemId: itemId
|
||||
},
|
||||
query: {
|
||||
userId: this.getUserId(),
|
||||
startTimeTicks,
|
||||
autoOpenLiveStream: true,
|
||||
maxStreamingBitrate
|
||||
}
|
||||
},
|
||||
body: {
|
||||
DeviceProfile: playbackProfile
|
||||
}
|
||||
})
|
||||
.then((r) => ({
|
||||
playbackUri:
|
||||
r.data?.MediaSources?.[0]?.TranscodingUrl ||
|
||||
`/Videos/${r.data?.MediaSources?.[0]?.Id}/stream.mp4?Static=true&mediaSourceId=${
|
||||
r.data?.MediaSources?.[0]?.Id
|
||||
}&deviceId=${JELLYFIN_DEVICE_ID}&api_key=${this.getApiKey()}&Tag=${
|
||||
r.data?.MediaSources?.[0]?.ETag
|
||||
}`,
|
||||
mediaSourceId: r.data?.MediaSources?.[0]?.Id,
|
||||
playSessionId: r.data?.PlaySessionId,
|
||||
directPlay:
|
||||
!!r.data?.MediaSources?.[0]?.SupportsDirectPlay ||
|
||||
!!r.data?.MediaSources?.[0]?.SupportsDirectStream
|
||||
}));
|
||||
|
||||
reportPlaybackStarted(
|
||||
itemId: string,
|
||||
sessionId: string,
|
||||
mediaSourceId: string,
|
||||
audioStreamIndex?: number,
|
||||
subtitleStreamIndex?: number
|
||||
) {
|
||||
return this.getClient()?.POST('/Sessions/Playing', {
|
||||
body: {
|
||||
CanSeek: true,
|
||||
ItemId: itemId,
|
||||
PlaySessionId: sessionId,
|
||||
MediaSourceId: mediaSourceId,
|
||||
AudioStreamIndex: 1,
|
||||
SubtitleStreamIndex: -1
|
||||
}
|
||||
});
|
||||
}
|
||||
reportPlaybackProgress(
|
||||
itemId: string,
|
||||
sessionId: string,
|
||||
isPaused: boolean,
|
||||
positionTicks: number
|
||||
) {
|
||||
return this.getClient()?.POST('/Sessions/Playing/Progress', {
|
||||
body: {
|
||||
ItemId: itemId,
|
||||
PlaySessionId: sessionId,
|
||||
IsPaused: isPaused,
|
||||
PositionTicks: Math.round(positionTicks),
|
||||
CanSeek: true,
|
||||
MediaSourceId: itemId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reportPlaybackStopped(itemId: string, sessionId: string, positionTicks: number) {
|
||||
return this.getClient()?.POST('/Sessions/Playing/Stopped', {
|
||||
body: {
|
||||
ItemId: itemId,
|
||||
PlaySessionId: sessionId,
|
||||
PositionTicks: Math.round(positionTicks),
|
||||
MediaSourceId: itemId
|
||||
}
|
||||
});
|
||||
}
|
||||
deleteActiveEncoding(playSessionId: string) {
|
||||
return this.getClient()?.DELETE('/Videos/ActiveEncodings', {
|
||||
params: {
|
||||
query: {
|
||||
deviceId: JELLYFIN_DEVICE_ID,
|
||||
playSessionId: playSessionId
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const jellyfinApi = new JellyfinApi();
|
||||
|
||||
17
src/lib/components/Button.svelte
Normal file
17
src/lib/components/Button.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import Container from '../../Container.svelte';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import classNames from 'classnames';
|
||||
let hasFoucus: Readable<boolean>;
|
||||
</script>
|
||||
|
||||
<Container
|
||||
bind:hasFocus={hasFoucus}
|
||||
class={classNames('selectable px-6 py-2 rounded-xl font-medium tracking-wide', {
|
||||
'bg-stone-200 text-stone-900': $hasFoucus,
|
||||
'bg-stone-800 text-white': !$hasFoucus
|
||||
})}
|
||||
on:click
|
||||
>
|
||||
<slot />
|
||||
</Container>
|
||||
@@ -40,7 +40,7 @@
|
||||
<Laptop class="w-8 h-8" slot="icon" />
|
||||
</div>
|
||||
</Container>
|
||||
<Container on:click={() => navigate('/movie/76600')}>
|
||||
<Container on:click={() => navigate('/movie/695721')}>
|
||||
<div class={itemContainer(1, $focusIndex)}>
|
||||
<CardStack class="w-8 h-8" slot="icon" />
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,9 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
delteActiveEncoding as deleteActiveEncoding,
|
||||
getJellyfinItem,
|
||||
getJellyfinPlaybackInfo,
|
||||
reportJellyfinPlaybackProgress,
|
||||
reportJellyfinPlaybackStarted,
|
||||
reportJellyfinPlaybackStopped
|
||||
} from '../../apis/jellyfin/jellyfin-api';
|
||||
import getDeviceProfile from '../../apis/jellyfin/playback-profiles';
|
||||
import { getQualities } from '../../apis/jellyfin/qualities';
|
||||
import { settings } from '../../stores/settings.store';
|
||||
import classNames from 'classnames';
|
||||
import Hls from 'hls.js';
|
||||
import {
|
||||
Cross2,
|
||||
EnterFullScreen,
|
||||
ExitFullScreen,
|
||||
Gear,
|
||||
@@ -33,6 +23,9 @@
|
||||
import { linear } from 'svelte/easing';
|
||||
import ContextMenuButton from '../ContextMenu/ContextMenuButton.svelte';
|
||||
import { isTizen } from '../../utils/browser-detection';
|
||||
import { jellyfinApi } from '../../apis/jellyfin/jellyfin-api.js';
|
||||
import { videoPlayerSettings } from '../../stores/localstorage.store';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export let jellyfinId: string;
|
||||
|
||||
@@ -54,7 +47,8 @@
|
||||
|
||||
// Find the correct functions
|
||||
let elem = document.createElement('div');
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
if (elem.requestFullscreen) {
|
||||
reqFullscreenFunc = (elem) => {
|
||||
elem.requestFullscreen();
|
||||
@@ -62,41 +56,55 @@
|
||||
fullscreenChangeEvent = 'fullscreenchange';
|
||||
getFullscreenElement = () => <HTMLElement>document.fullscreenElement;
|
||||
if (document.exitFullscreen) exitFullscreen = () => document.exitFullscreen();
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
} else if (elem.webkitRequestFullscreen) {
|
||||
reqFullscreenFunc = (elem) => {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
elem.webkitRequestFullscreen();
|
||||
};
|
||||
fullscreenChangeEvent = 'webkitfullscreenchange';
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
getFullscreenElement = () => <HTMLElement>document.webkitFullscreenElement;
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
if (document.webkitExitFullscreen) exitFullscreen = () => document.webkitExitFullscreen();
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
} else if (elem.msRequestFullscreen) {
|
||||
reqFullscreenFunc = (elem) => {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
elem.msRequestFullscreen();
|
||||
};
|
||||
fullscreenChangeEvent = 'MSFullscreenChange';
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
getFullscreenElement = () => <HTMLElement>document.msFullscreenElement;
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
if (document.msExitFullscreen) exitFullscreen = () => document.msExitFullscreen();
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
} else if (elem.mozRequestFullScreen) {
|
||||
reqFullscreenFunc = (elem) => {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
elem.mozRequestFullScreen();
|
||||
};
|
||||
fullscreenChangeEvent = 'mozfullscreenchange';
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
getFullscreenElement = () => <HTMLElement>document.mozFullScreenElement;
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
if (document.mozCancelFullScreen) exitFullscreen = () => document.mozCancelFullScreen();
|
||||
}
|
||||
|
||||
const inintialValues = get(videoPlayerSettings);
|
||||
|
||||
let paused: boolean = false;
|
||||
let duration: number = 0;
|
||||
let displayedTime: number = 0;
|
||||
@@ -107,8 +115,8 @@
|
||||
let playerStateBeforeSeek: boolean;
|
||||
|
||||
let fullscreen: boolean = false;
|
||||
let volume: number = 1;
|
||||
let mute: boolean = false;
|
||||
let volume: number = inintialValues.volume;
|
||||
let mute: boolean = inintialValues.muted;
|
||||
|
||||
let resolution: number = 1080;
|
||||
let currentBitrate: number = 0;
|
||||
@@ -117,92 +125,96 @@
|
||||
let uiVisible = true;
|
||||
$: uiVisible = !shouldCloseUi || seeking || paused || $contextMenu === qualityContextMenuId;
|
||||
|
||||
$: videoPlayerSettings.set({ volume, muted: mute });
|
||||
|
||||
const fetchPlaybackInfo = (
|
||||
itemId: string,
|
||||
maxBitrate: number | undefined = undefined,
|
||||
starting: boolean = true
|
||||
) =>
|
||||
getJellyfinItem(itemId).then((item) =>
|
||||
getJellyfinPlaybackInfo(
|
||||
itemId,
|
||||
getDeviceProfile(),
|
||||
item?.UserData?.PlaybackPositionTicks || Math.floor(displayedTime * 10_000_000),
|
||||
maxBitrate || getQualities(item?.Height || 1080)[0].maxBitrate
|
||||
).then(async (playbackInfo) => {
|
||||
if (!playbackInfo) return;
|
||||
const { playbackUri, playSessionId: sessionId, mediaSourceId, directPlay } = playbackInfo;
|
||||
jellyfinApi.getLibraryItem(itemId).then((item) =>
|
||||
jellyfinApi
|
||||
.getPlaybackInfo(
|
||||
itemId,
|
||||
getDeviceProfile(),
|
||||
item?.UserData?.PlaybackPositionTicks || Math.floor(displayedTime * 10_000_000),
|
||||
maxBitrate || getQualities(item?.Height || 1080)[0]?.maxBitrate
|
||||
)
|
||||
.then(async (playbackInfo) => {
|
||||
if (!playbackInfo) return;
|
||||
const { playbackUri, playSessionId: sessionId, mediaSourceId, directPlay } = playbackInfo;
|
||||
|
||||
if (!playbackUri || !sessionId) {
|
||||
console.log('No playback URL or session ID', playbackUri, sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
video.poster = item?.BackdropImageTags?.length
|
||||
? `${$settings.jellyfin.baseUrl}/Items/${item?.Id}/Images/Backdrop?quality=100&tag=${item?.BackdropImageTags?.[0]}`
|
||||
: '';
|
||||
|
||||
videoLoaded = false;
|
||||
if (!directPlay) {
|
||||
if (Hls.isSupported()) {
|
||||
const hls = new Hls();
|
||||
|
||||
hls.loadSource($settings.jellyfin.baseUrl + playbackUri);
|
||||
hls.attachMedia(video);
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl') || isTizen()) {
|
||||
/*
|
||||
* HLS.js does NOT work on iOS on iPhone because Safari on iPhone does not support MSE.
|
||||
* This is not a problem, since HLS is natively supported on iOS. But any other browser
|
||||
* that does not support MSE will not be able to play the video.
|
||||
*/
|
||||
video.src = $settings.jellyfin.baseUrl + playbackUri;
|
||||
} else {
|
||||
throw new Error('HLS is not supported');
|
||||
if (!playbackUri || !sessionId) {
|
||||
console.log('No playback URL or session ID', playbackUri, sessionId);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
video.src = $settings.jellyfin.baseUrl + playbackUri;
|
||||
}
|
||||
|
||||
resolution = item?.Height || 1080;
|
||||
currentBitrate = maxBitrate || getQualities(resolution)[0].maxBitrate;
|
||||
video.poster = item?.BackdropImageTags?.length
|
||||
? `${$settings.jellyfin.baseUrl}/Items/${item?.Id}/Images/Backdrop?quality=100&tag=${item?.BackdropImageTags?.[0]}`
|
||||
: '';
|
||||
|
||||
if (item?.UserData?.PlaybackPositionTicks) {
|
||||
displayedTime = item?.UserData?.PlaybackPositionTicks / 10_000_000;
|
||||
}
|
||||
videoLoaded = false;
|
||||
if (!directPlay) {
|
||||
if (Hls.isSupported()) {
|
||||
const hls = new Hls();
|
||||
|
||||
// We should not requestFullscreen automatically, as it's not what
|
||||
// the user expects. Moreover, most browsers will deny the request
|
||||
// if the video takes a while to load.
|
||||
// video.play().then(() => videoWrapper.requestFullscreen());
|
||||
hls.loadSource($settings.jellyfin.baseUrl + playbackUri);
|
||||
hls.attachMedia(video);
|
||||
} else if (video.canPlayType('application/vnd.apple.mpegurl') || isTizen()) {
|
||||
/*
|
||||
* HLS.js does NOT work on iOS on iPhone because Safari on iPhone does not support MSE.
|
||||
* This is not a problem, since HLS is natively supported on iOS. But any other browser
|
||||
* that does not support MSE will not be able to play the video.
|
||||
*/
|
||||
video.src = $settings.jellyfin.baseUrl + playbackUri;
|
||||
} else {
|
||||
throw new Error('HLS is not supported');
|
||||
}
|
||||
} else {
|
||||
video.src = $settings.jellyfin.baseUrl + playbackUri;
|
||||
}
|
||||
|
||||
// A start report should only be sent when the video starts playing,
|
||||
// not every time a playback info request is made
|
||||
if (mediaSourceId && starting)
|
||||
await reportJellyfinPlaybackStarted(itemId, sessionId, mediaSourceId);
|
||||
resolution = item?.Height || 1080;
|
||||
currentBitrate = maxBitrate || getQualities(resolution)[0]?.maxBitrate;
|
||||
|
||||
reportProgress = async () => {
|
||||
await reportJellyfinPlaybackProgress(
|
||||
itemId,
|
||||
sessionId,
|
||||
video?.paused == true,
|
||||
video?.currentTime * 10_000_000
|
||||
);
|
||||
};
|
||||
if (item?.UserData?.PlaybackPositionTicks) {
|
||||
displayedTime = item?.UserData?.PlaybackPositionTicks / 10_000_000;
|
||||
}
|
||||
|
||||
if (progressInterval) clearInterval(progressInterval);
|
||||
progressInterval = setInterval(() => {
|
||||
video && video.readyState === 4 && video?.currentTime > 0 && sessionId && itemId;
|
||||
reportProgress();
|
||||
}, 5000);
|
||||
// We should not requestFullscreen automatically, as it's not what
|
||||
// the user expects. Moreover, most browsers will deny the request
|
||||
// if the video takes a while to load.
|
||||
// video.play().then(() => videoWrapper.requestFullscreen());
|
||||
|
||||
deleteEncoding = () => {
|
||||
deleteActiveEncoding(sessionId);
|
||||
};
|
||||
// A start report should only be sent when the video starts playing,
|
||||
// not every time a playback info request is made
|
||||
if (mediaSourceId && starting)
|
||||
await jellyfinApi.reportPlaybackStarted(itemId, sessionId, mediaSourceId);
|
||||
|
||||
stopCallback = () => {
|
||||
reportJellyfinPlaybackStopped(itemId, sessionId, video?.currentTime * 10_000_000);
|
||||
deleteEncoding();
|
||||
};
|
||||
})
|
||||
reportProgress = async () => {
|
||||
await jellyfinApi.reportPlaybackProgress(
|
||||
itemId,
|
||||
sessionId,
|
||||
video?.paused == true,
|
||||
video?.currentTime * 10_000_000
|
||||
);
|
||||
};
|
||||
|
||||
if (progressInterval) clearInterval(progressInterval);
|
||||
progressInterval = setInterval(() => {
|
||||
video && video.readyState === 4 && video?.currentTime > 0 && sessionId && itemId;
|
||||
reportProgress();
|
||||
}, 5000);
|
||||
|
||||
deleteEncoding = () => {
|
||||
jellyfinApi.deleteActiveEncoding(sessionId);
|
||||
};
|
||||
|
||||
stopCallback = () => {
|
||||
jellyfinApi.reportPlaybackStopped(itemId, sessionId, video?.currentTime * 10_000_000);
|
||||
deleteEncoding();
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
function onSeekStart() {
|
||||
@@ -294,17 +306,14 @@
|
||||
$: {
|
||||
if (video && jellyfinId) {
|
||||
if (video.src === '') fetchPlaybackInfo(jellyfinId);
|
||||
paused = false;
|
||||
console.log('Paused', paused);
|
||||
video.play();
|
||||
// video.play();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
// Workaround because the paused state does not sync
|
||||
// with the video element until a change is made
|
||||
paused = false;
|
||||
|
||||
// paused = false;
|
||||
// if (video && $playerState.jellyfinId) {
|
||||
// if (video.src === '') fetchPlaybackInfo($playerState.jellyfinId);
|
||||
// }
|
||||
@@ -345,7 +354,7 @@
|
||||
function handleShortcuts(event: KeyboardEvent) {
|
||||
if (event.key === 'f') {
|
||||
handleRequestFullscreen();
|
||||
} else if (event.key === ' ') {
|
||||
} else if (event.key === ' ' || event.key === 'k') {
|
||||
paused = !paused;
|
||||
} else if (event.key === 'ArrowLeft') {
|
||||
video.currentTime -= 10;
|
||||
@@ -355,6 +364,8 @@
|
||||
volume = Math.min(volume + 0.1, 1);
|
||||
} else if (event.key === 'ArrowDown') {
|
||||
volume = Math.max(volume - 0.1, 0);
|
||||
} else if (event.key === 'm') {
|
||||
mute = !mute;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -374,7 +385,7 @@
|
||||
<!-->-->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="w-screen h-screen flex items-center justify-center"
|
||||
class="w-full h-full flex items-center justify-center relative"
|
||||
bind:this={videoWrapper}
|
||||
on:mousemove={() => handleUserInteraction(false)}
|
||||
on:touchend|preventDefault={() => handleUserInteraction(true)}
|
||||
@@ -407,7 +418,7 @@
|
||||
{#if uiVisible}
|
||||
<!-- Video controls -->
|
||||
<div
|
||||
class="absolute bottom-0 w-screen bg-gradient-to-t from-black/[.8] via-60% via-black-opacity-80 to-transparent"
|
||||
class="absolute bottom-0 inset-x-0 bg-gradient-to-t from-black/[.8] via-60% via-black-opacity-80 to-transparent"
|
||||
on:touchend|stopPropagation
|
||||
transition:fade={{ duration: 100 }}
|
||||
>
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
import { TMDB_IMAGES_ORIGINAL } from '../constants';
|
||||
import classNames from 'classnames';
|
||||
import { DotFilled } from 'radix-icons-svelte';
|
||||
import SidebarMargin from '../components/SidebarMargin.svelte';
|
||||
import Button from '../components/Button.svelte';
|
||||
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
|
||||
import VideoPlayer from '../components/VideoPlayer/VideoPlayer.svelte';
|
||||
|
||||
export let id: string;
|
||||
|
||||
const movieDataP = tmdbApi.getTmdbMovie(Number(id));
|
||||
const jellyfinItem = jellyfinApi.getLibraryItemFromTmdbId(id);
|
||||
|
||||
let playbackId: string = '';
|
||||
|
||||
let heroIndex: number;
|
||||
</script>
|
||||
@@ -60,8 +65,19 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
{#await jellyfinItem then item}
|
||||
{#if item}
|
||||
<div class="flex mt-4">
|
||||
<Button on:click={() => (playbackId = item.Id || '')}>Play</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</HeroCarousel>
|
||||
</div>
|
||||
<Container on:click={() => history.back()}>Go back</Container>
|
||||
<div>
|
||||
{#if playbackId}
|
||||
<VideoPlayer jellyfinId={playbackId} />
|
||||
{/if}
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -25,7 +25,7 @@ type AwaitableStoreValue<R, T = { data?: R }> = {
|
||||
loading: boolean;
|
||||
} & T;
|
||||
|
||||
function _createDataFetchStore<T>(fn: () => Promise<T>) {
|
||||
export function _createDataFetchStore<T>(fn: () => Promise<T>) {
|
||||
const store = writable<AwaitableStoreValue<T>>({
|
||||
loading: true,
|
||||
data: undefined
|
||||
|
||||
@@ -22,3 +22,10 @@ export function createLocalStorageStore<T>(key: string, defaultValue: T) {
|
||||
}
|
||||
|
||||
export const skippedVersion = createLocalStorageStore<string | null>('skipped-version', null);
|
||||
export const videoPlayerSettings = createLocalStorageStore<{
|
||||
muted: boolean;
|
||||
volume: number;
|
||||
}>('video-player-settings', {
|
||||
muted: false,
|
||||
volume: 1
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user