mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-17 23:53:13 +02:00
feat: Loading source plugins
This commit is contained in:
@@ -23,7 +23,7 @@ module.exports = {
|
|||||||
files: ['*.svelte'],
|
files: ['*.svelte'],
|
||||||
parser: 'svelte-eslint-parser',
|
parser: 'svelte-eslint-parser',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
parser: '@typescript-eslint/parser',
|
parser: '@typescript-eslint/parser'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -31,6 +31,6 @@ module.exports = {
|
|||||||
'@typescript-eslint/ban-ts-comment': 'off',
|
'@typescript-eslint/ban-ts-comment': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': 'warn',
|
'@typescript-eslint/no-unused-vars': 'warn',
|
||||||
'@typescript-eslint/no-explicit-any': 'warn',
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
'prefer-const': 'warn',
|
'prefer-const': 'warn'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -60,8 +60,8 @@ jobs:
|
|||||||
- name: Add tag
|
- name: Add tag
|
||||||
run: docker tag ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:latest ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:${{env.TAG}}
|
run: docker tag ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:latest ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:${{env.TAG}}
|
||||||
|
|
||||||
# - name: Add tag
|
# - name: Add tag
|
||||||
# run: docker tag ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:latest ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:${{github.sha}}
|
# run: docker tag ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:latest ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:${{github.sha}}
|
||||||
|
|
||||||
# - name: Tag with Git SHA
|
# - name: Tag with Git SHA
|
||||||
# run: docker tag ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:latest ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:${{github.sha}}
|
# run: docker tag ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:latest ${{env.REGISTRY}}/${{env.IMAGE_NAME}}:${{github.sha}}
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -104,12 +104,12 @@ the app using Tizen Studio or the CLI, following roughly these steps:
|
|||||||
2. Download either Tizen Studio or the CLI tools from the [official website](https://developer.tizen.org/development/tizen-studio/download)
|
2. Download either Tizen Studio or the CLI tools from the [official website](https://developer.tizen.org/development/tizen-studio/download)
|
||||||
3. [Connect Tizen Studio to your TV](https://developer.samsung.com/smarttv/develop/getting-started/using-sdk/tv-device.html)
|
3. [Connect Tizen Studio to your TV](https://developer.samsung.com/smarttv/develop/getting-started/using-sdk/tv-device.html)
|
||||||
4. Use the following command to build and install the app on your TV:\
|
4. Use the following command to build and install the app on your TV:\
|
||||||
\
|
\
|
||||||
`npm run build:tizen;C:\tizen-studio\tools\ide\bin\tizen.bat build-web -- tizen;C:\tizen-studio\tools\ide\bin\tizen.bat package -t wgt -o .\tizen -- .\tizen\.buildResult\;C:\tizen-studio\tools\ide\bin\tizen.bat install -n .\tizen\Reiverr.wgt -t QE55Q64TAUXXC`.\
|
`npm run build:tizen;C:\tizen-studio\tools\ide\bin\tizen.bat build-web -- tizen;C:\tizen-studio\tools\ide\bin\tizen.bat package -t wgt -o .\tizen -- .\tizen\.buildResult\;C:\tizen-studio\tools\ide\bin\tizen.bat install -n .\tizen\Reiverr.wgt -t QE55Q64TAUXXC`.\
|
||||||
\
|
\
|
||||||
You may need to replace the paths for Tizen Studio tools according to your installation location, as well as the device identifier, which was in my case the tv model number.\
|
You may need to replace the paths for Tizen Studio tools according to your installation location, as well as the device identifier, which was in my case the tv model number.\
|
||||||
\
|
\
|
||||||
Alternatively, you can open the project in Tizen Studio and install the project on a device from there. For more instructions on run a project on a device, see [here](https://docs.tizen.org/application/web/get-started/tv/first-samsung-tv-app/#run-on-a-target-device).
|
Alternatively, you can open the project in Tizen Studio and install the project on a device from there. For more instructions on run a project on a device, see [here](https://docs.tizen.org/application/web/get-started/tv/first-samsung-tv-app/#run-on-a-target-device).
|
||||||
|
|
||||||
If you have any questions or run into issues or bugs, you can start a [discussion](https://github.com/aleksilassila/reiverr/discussions),
|
If you have any questions or run into issues or bugs, you can start a [discussion](https://github.com/aleksilassila/reiverr/discussions),
|
||||||
open an [issue](https://github.com/aleksilassila/reiverr/issues)
|
open an [issue](https://github.com/aleksilassila/reiverr/issues)
|
||||||
@@ -134,7 +134,6 @@ To get most out of Reiverr, it is recommended to connect to TMDB, Jellyfin, Rada
|
|||||||
> Hint: Radarr & Sonarr API keys can be found under Settings > General in their respective web UIs.
|
> Hint: Radarr & Sonarr API keys can be found under Settings > General in their respective web UIs.
|
||||||
> Jellyfin API key is located under Administration > Dashboard > Advanced > API Keys in the Jellyfin Web UI.
|
> Jellyfin API key is located under Administration > Dashboard > Advanced > API Keys in the Jellyfin Web UI.
|
||||||
|
|
||||||
|
|
||||||
# Contributing
|
# Contributing
|
||||||
|
|
||||||
Unlike the most Servarr projects, this one is built with Svelte and NestJS. If you haven't used Svelte before,
|
Unlike the most Servarr projects, this one is built with Svelte and NestJS. If you haven't used Svelte before,
|
||||||
@@ -175,6 +174,7 @@ To get started with development:
|
|||||||
5. To start the backend: `npm run --prefix backend start:dev`
|
5. To start the backend: `npm run --prefix backend start:dev`
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- 2.0 will primarily target TVs, so the UI must be optimized with TVs in mind. This means larger text, buttons, etc.
|
- 2.0 will primarily target TVs, so the UI must be optimized with TVs in mind. This means larger text, buttons, etc.
|
||||||
[Design Guide for Android TV](https://developer.android.com/design/ui/tv/guides/styles/layouts) is a good resource
|
[Design Guide for Android TV](https://developer.android.com/design/ui/tv/guides/styles/layouts) is a good resource
|
||||||
for how to design for TVs.
|
for how to design for TVs.
|
||||||
@@ -185,6 +185,7 @@ To get started with development:
|
|||||||
css `gap` property. You can use the `space-x` and `space-y` classes from Tailwind CSS to achieve the same effect.
|
css `gap` property. You can use the `space-x` and `space-y` classes from Tailwind CSS to achieve the same effect.
|
||||||
|
|
||||||
## Useful resources
|
## Useful resources
|
||||||
|
|
||||||
- https://developer.themoviedb.org/reference
|
- https://developer.themoviedb.org/reference
|
||||||
- https://api.jellyfin.org/
|
- https://api.jellyfin.org/
|
||||||
- https://sonarr.tv/docs/api/
|
- https://sonarr.tv/docs/api/
|
||||||
|
|||||||
0
backend/.eslintrc.js
Normal file → Executable file
0
backend/.eslintrc.js
Normal file → Executable file
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"singleQuote": true,
|
|
||||||
"trailingComma": "all"
|
|
||||||
}
|
|
||||||
@@ -59,10 +59,12 @@ $ npm run test:cov
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- Routes
|
- Routes
|
||||||
- series -> shows
|
- series -> shows
|
||||||
|
|
||||||
### Users
|
### Users
|
||||||
|
|
||||||
- PUT /users/{id}
|
- PUT /users/{id}
|
||||||
- GET /users
|
- GET /users
|
||||||
- GET /users/{id}
|
- GET /users/{id}
|
||||||
@@ -70,30 +72,36 @@ $ npm run test:cov
|
|||||||
- POST /users
|
- POST /users
|
||||||
|
|
||||||
### Users > Library
|
### Users > Library
|
||||||
|
|
||||||
- GET /users/{userId}/library
|
- GET /users/{userId}/library
|
||||||
- POST /users/{userId}/library/{tmdbId}
|
- POST /users/{userId}/library/{tmdbId}
|
||||||
- DELETE /users/{userId}/library/{tmdbId}
|
- DELETE /users/{userId}/library/{tmdbId}
|
||||||
|
|
||||||
### Users > PlayState
|
### Users > PlayState
|
||||||
|
|
||||||
- GET /users/{userId}/playstate/movies/{tmdbId}
|
- GET /users/{userId}/playstate/movies/{tmdbId}
|
||||||
- GET /users/{userId}/playstate/series/{tmdbId}/{season}/{episode}
|
- GET /users/{userId}/playstate/series/{tmdbId}/{season}/{episode}
|
||||||
- PUT /users/{userId}/playstate/movies/{tmdbId}
|
- PUT /users/{userId}/playstate/movies/{tmdbId}
|
||||||
- PUT /users/{userId}/playstate/series/{tmdbId}/{season}/{episode}
|
- PUT /users/{userId}/playstate/series/{tmdbId}/{season}/{episode}
|
||||||
|
|
||||||
### TMDB / Metadata
|
### TMDB / Metadata
|
||||||
|
|
||||||
- GET /movies/{tmdbId}
|
- GET /movies/{tmdbId}
|
||||||
- GET /movies/{tmdbId}/similar
|
- GET /movies/{tmdbId}/similar
|
||||||
- GET /movies/recommendations
|
|
||||||
- User-specific
|
|
||||||
- GET /series/{tmdbId}
|
- GET /series/{tmdbId}
|
||||||
- (GET /series/{tmdbId}/season/{season})
|
- (GET /series/{tmdbId}/season/{season})
|
||||||
- GET /series/{tmdbId}/season/{season}/episode/{episode}
|
- GET /series/{tmdbId}/season/{season}/episode/{episode}
|
||||||
- GET /series/{tmdbId}/similar
|
- GET /series/{tmdbId}/similar
|
||||||
- GET /series/recommendations
|
|
||||||
- User-specific
|
|
||||||
- GET /people/{tmdbPersonId}
|
- GET /people/{tmdbPersonId}
|
||||||
|
|
||||||
### TMDB / Discovery
|
### TMDB / Discovery
|
||||||
|
|
||||||
|
- GET /discover/movies
|
||||||
|
- User-specific
|
||||||
|
- GET /discover/series
|
||||||
|
|
||||||
|
- User-specific
|
||||||
|
|
||||||
- (GET /discover/popular)
|
- (GET /discover/popular)
|
||||||
- GET /discover/popular/movies
|
- GET /discover/popular/movies
|
||||||
- GET /discover/popular/series
|
- GET /discover/popular/series
|
||||||
@@ -105,6 +113,7 @@ $ npm run test:cov
|
|||||||
- GET /discover/genre/{genre}/series
|
- GET /discover/genre/{genre}/series
|
||||||
|
|
||||||
### Sources
|
### Sources
|
||||||
|
|
||||||
- GET /sources
|
- GET /sources
|
||||||
- (GET /sources/{sourceId})
|
- (GET /sources/{sourceId})
|
||||||
- GET /sources/{sourceId}/movies
|
- GET /sources/{sourceId}/movies
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
export class AddProfilePicture1718397928862 implements MigrationInterface {
|
export class AddProfilePicture1718397928862 implements MigrationInterface {
|
||||||
name = 'AddProfilePicture1718397928862'
|
name = 'AddProfilePicture1718397928862';
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`CREATE TABLE "temporary_user" ("id" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "password" varchar NOT NULL, "isAdmin" boolean NOT NULL DEFAULT (0), "onboardingDone" boolean NOT NULL DEFAULT (0), "settings" json NOT NULL DEFAULT ('{"autoplayTrailers":true,"language":"en","animationDuration":300,"sonarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":"","languageProfileId":0},"radarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":""},"jellyfin":{"apiKey":"","baseUrl":"","userId":""},"tmdb":{"sessionId":"","userId":""}}'), "profilePicture" blob, CONSTRAINT "UQ_065d4d8f3b5adb4a08841eae3c8" UNIQUE ("name"))`);
|
await queryRunner.query(
|
||||||
await queryRunner.query(`INSERT INTO "temporary_user"("id", "name", "password", "isAdmin", "onboardingDone", "settings") SELECT "id", "name", "password", "isAdmin", "onboardingDone", "settings" FROM "user"`);
|
`CREATE TABLE "temporary_user" ("id" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "password" varchar NOT NULL, "isAdmin" boolean NOT NULL DEFAULT (0), "onboardingDone" boolean NOT NULL DEFAULT (0), "settings" json NOT NULL DEFAULT ('{"autoplayTrailers":true,"language":"en","animationDuration":300,"sonarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":"","languageProfileId":0},"radarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":""},"jellyfin":{"apiKey":"","baseUrl":"","userId":""},"tmdb":{"sessionId":"","userId":""}}'), "profilePicture" blob, CONSTRAINT "UQ_065d4d8f3b5adb4a08841eae3c8" UNIQUE ("name"))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "temporary_user"("id", "name", "password", "isAdmin", "onboardingDone", "settings") SELECT "id", "name", "password", "isAdmin", "onboardingDone", "settings" FROM "user"`,
|
||||||
|
);
|
||||||
await queryRunner.query(`DROP TABLE "user"`);
|
await queryRunner.query(`DROP TABLE "user"`);
|
||||||
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
|
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
|
await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`);
|
||||||
await queryRunner.query(`CREATE TABLE "user" ("id" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "password" varchar NOT NULL, "isAdmin" boolean NOT NULL DEFAULT (0), "onboardingDone" boolean NOT NULL DEFAULT (0), "settings" json NOT NULL DEFAULT ('{"autoplayTrailers":true,"language":"en","animationDuration":300,"sonarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":"","languageProfileId":0},"radarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":""},"jellyfin":{"apiKey":"","baseUrl":"","userId":""},"tmdb":{"sessionId":"","userId":""}}'), CONSTRAINT "UQ_065d4d8f3b5adb4a08841eae3c8" UNIQUE ("name"))`);
|
await queryRunner.query(
|
||||||
await queryRunner.query(`INSERT INTO "user"("id", "name", "password", "isAdmin", "onboardingDone", "settings") SELECT "id", "name", "password", "isAdmin", "onboardingDone", "settings" FROM "temporary_user"`);
|
`CREATE TABLE "user" ("id" varchar PRIMARY KEY NOT NULL, "name" varchar NOT NULL, "password" varchar NOT NULL, "isAdmin" boolean NOT NULL DEFAULT (0), "onboardingDone" boolean NOT NULL DEFAULT (0), "settings" json NOT NULL DEFAULT ('{"autoplayTrailers":true,"language":"en","animationDuration":300,"sonarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":"","languageProfileId":0},"radarr":{"apiKey":"","baseUrl":"","qualityProfileId":0,"rootFolderPath":""},"jellyfin":{"apiKey":"","baseUrl":"","userId":""},"tmdb":{"sessionId":"","userId":""}}'), CONSTRAINT "UQ_065d4d8f3b5adb4a08841eae3c8" UNIQUE ("name"))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`INSERT INTO "user"("id", "name", "password", "isAdmin", "onboardingDone", "settings") SELECT "id", "name", "password", "isAdmin", "onboardingDone", "settings" FROM "temporary_user"`,
|
||||||
|
);
|
||||||
await queryRunner.query(`DROP TABLE "temporary_user"`);
|
await queryRunner.query(`DROP TABLE "temporary_user"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
"collection": "@nestjs/schematics",
|
"collection": "@nestjs/schematics",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"deleteOutDir": true
|
"deleteOutDir": true,
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"include": "../plugins/**",
|
||||||
|
"outDir": "dist/plugins",
|
||||||
|
"watchAssets": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
backend/package-lock.json
generated
12
backend/package-lock.json
generated
@@ -8236,9 +8236,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.2.5",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
|
||||||
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
|
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
@@ -10173,9 +10173,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.4.3",
|
"version": "5.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||||
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
|
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
|
|||||||
@@ -79,5 +79,11 @@
|
|||||||
],
|
],
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25462
backend/plugins/jellyfin.plugin/api/jellyfin.openapi.ts
Normal file
25462
backend/plugins/jellyfin.plugin/api/jellyfin.openapi.ts
Normal file
File diff suppressed because it is too large
Load Diff
118
backend/plugins/jellyfin.plugin/index.ts
Normal file
118
backend/plugins/jellyfin.plugin/index.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { generateApi, generateTemplates } from 'swagger-typescript-api';
|
||||||
|
import {
|
||||||
|
BaseItemKind,
|
||||||
|
ItemFields,
|
||||||
|
Api as JellyfinApi,
|
||||||
|
} from './api/jellyfin.openapi';
|
||||||
|
|
||||||
|
export interface SourcePlugin {
|
||||||
|
handleProxy(request: { uri: string; headers: any }): any;
|
||||||
|
name: string;
|
||||||
|
indexable: boolean;
|
||||||
|
getMovieStream: (tmdbId: string) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: 'http://192.168.0.129:8096',
|
||||||
|
userId: '',
|
||||||
|
};
|
||||||
|
export const JELLYFIN_DEVICE_ID = 'Reiverr Client';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export default class JellyfinPlugin implements SourcePlugin {
|
||||||
|
name: string = 'jellyfin';
|
||||||
|
indexable: boolean = true;
|
||||||
|
|
||||||
|
api: JellyfinApi<{}>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
generateApi({
|
||||||
|
name: 'jellyfin.openapi.ts',
|
||||||
|
url: 'https://api.jellyfin.org/openapi/jellyfin-openapi-stable.json',
|
||||||
|
output:
|
||||||
|
'/Users/aleksilassila/Workspace/Documents/node/reiverr/backend/plugins/jellyfin.plugin/api',
|
||||||
|
// generateClient: true,
|
||||||
|
// generateRouteTypes: false,
|
||||||
|
// sortTypes: true,
|
||||||
|
httpClientType: 'axios',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api = new JellyfinApi({
|
||||||
|
baseURL: config.baseUrl,
|
||||||
|
headers: {
|
||||||
|
Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${config.apiKey}"`,
|
||||||
|
},
|
||||||
|
paramsSerializer: {
|
||||||
|
indexes: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
handleProxy({ uri, headers }) {
|
||||||
|
return {
|
||||||
|
url: `${config.baseUrl}/${uri}`,
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${config.apiKey}"`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getLibraryItems() {
|
||||||
|
return this.api.items
|
||||||
|
.getItems({
|
||||||
|
userId: config.userId,
|
||||||
|
hasTmdbId: true,
|
||||||
|
recursive: true,
|
||||||
|
includeItemTypes: [BaseItemKind.Movie, BaseItemKind.Series],
|
||||||
|
fields: [
|
||||||
|
ItemFields.ProviderIds,
|
||||||
|
ItemFields.Genres,
|
||||||
|
ItemFields.DateLastMediaAdded,
|
||||||
|
ItemFields.DateCreated,
|
||||||
|
ItemFields.MediaSources,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
console.log(res.request.path);
|
||||||
|
return res;
|
||||||
|
})
|
||||||
|
.then((res) => res.data.Items ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMovieStream(tmdbId: string): Promise<string> {
|
||||||
|
const items = await this.getLibraryItems();
|
||||||
|
|
||||||
|
const movie = items.find((item) => item.ProviderIds?.Tmdb === tmdbId);
|
||||||
|
|
||||||
|
// console.log(items.map((item) => item))
|
||||||
|
|
||||||
|
if (!movie || !movie.MediaSources || movie.MediaSources.length === 0) {
|
||||||
|
throw new Error('Movie stream not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
await jellyfinApi.getPlaybackInfo(
|
||||||
|
id,
|
||||||
|
getDeviceProfile(),
|
||||||
|
options.playbackPosition || item?.UserData?.PlaybackPositionTicks || 0,
|
||||||
|
options.bitrate || getQualities(item?.Height || 1080)[0]?.maxBitrate,
|
||||||
|
audioStreamIndex
|
||||||
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
|
const playbackInfo = await this.api.items.getPlaybackInfo(movie.Id, {
|
||||||
|
userId: config.userId,
|
||||||
|
// deviceId: JELLYFIN_DEVICE_ID,
|
||||||
|
// mediaSourceId: movie.MediaSources[0].Id,
|
||||||
|
// maxBitrate: 8000000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const playbackUri =
|
||||||
|
playbackInfo.data?.MediaSources?.[0]?.TranscodingUrl ||
|
||||||
|
`/Videos/${playbackInfo.data?.MediaSources?.[0]?.Id}/stream.mp4?Static=true&mediaSourceId=${playbackInfo.data?.MediaSources?.[0]?.Id}&deviceId=${JELLYFIN_DEVICE_ID}&api_key=${config.apiKey}&Tag=${playbackInfo.data?.MediaSources?.[0]?.ETag}`;
|
||||||
|
|
||||||
|
return playbackUri;
|
||||||
|
}
|
||||||
|
}
|
||||||
3585
backend/plugins/jellyfin.plugin/package-lock.json
generated
Normal file
3585
backend/plugins/jellyfin.plugin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
backend/plugins/jellyfin.plugin/package.json
Normal file
17
backend/plugins/jellyfin.plugin/package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "jellyfin",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/axios": "^0.14.4",
|
||||||
|
"axios": "^1.7.8",
|
||||||
|
"express-http-proxy": "^2.1.1",
|
||||||
|
"swagger-typescript-api": "^13.0.23"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import { AuthModule } from './auth/auth.module';
|
|||||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { MediaModule } from './media/media.module';
|
import { MediaModule } from './media/media.module';
|
||||||
import { SourcesModule } from './sources/sources.module';
|
import { SourceModule } from './sources/sources.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -18,7 +18,7 @@ import { SourcesModule } from './sources/sources.module';
|
|||||||
rootPath: join(__dirname, '../dist'),
|
rootPath: join(__dirname, '../dist'),
|
||||||
}),
|
}),
|
||||||
MediaModule,
|
MediaModule,
|
||||||
SourcesModule,
|
SourceModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
UnauthorizedException,
|
UnauthorizedException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { JWT_SECRET } from '../consts';
|
import { ENV, JWT_SECRET } from '../consts';
|
||||||
import { AccessTokenPayload } from './auth.service';
|
import { AccessTokenPayload } from './auth.service';
|
||||||
import { User } from '../users/user.entity';
|
import { User } from '../users/user.entity';
|
||||||
import { UsersService } from '../users/users.service';
|
import { UsersService } from '../users/users.service';
|
||||||
@@ -34,7 +34,11 @@ export class AuthGuard implements CanActivate {
|
|||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const request = context.switchToHttp().getRequest();
|
const request = context.switchToHttp().getRequest();
|
||||||
const token = extractTokenFromHeader(request);
|
const token = extractTokenFromHeader(request);
|
||||||
if (!token) {
|
|
||||||
|
if (ENV === 'development' && !token) {
|
||||||
|
request['user'] = await this.userService.findOneByName('test');
|
||||||
|
return true;
|
||||||
|
} else if (!token) {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ export const JWT_SECRET =
|
|||||||
process.env.SECRET || Math.random().toString(36).substring(2, 15);
|
process.env.SECRET || Math.random().toString(36).substring(2, 15);
|
||||||
export const ADMIN_USERNAME = process.env.ADMIN_USERNAME;
|
export const ADMIN_USERNAME = process.env.ADMIN_USERNAME;
|
||||||
export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
|
export const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD;
|
||||||
|
export const ENV = process.env.NODE_ENV || 'development';
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { UsersService } from './users/users.service';
|
|||||||
import { ADMIN_PASSWORD, ADMIN_USERNAME } from './consts';
|
import { ADMIN_PASSWORD, ADMIN_USERNAME } from './consts';
|
||||||
import { json, urlencoded } from 'express';
|
import { json, urlencoded } from 'express';
|
||||||
// import * as proxy from 'express-http-proxy';
|
// import * as proxy from 'express-http-proxy';
|
||||||
|
require('ts-node/register'); // For importing plugins
|
||||||
|
|
||||||
async function createAdminUser(userService: UsersService) {
|
async function createAdminUser(userService: UsersService) {
|
||||||
if (!ADMIN_USERNAME || ADMIN_PASSWORD === undefined) return;
|
if (!ADMIN_USERNAME || ADMIN_PASSWORD === undefined) return;
|
||||||
@@ -42,6 +43,7 @@ async function bootstrap() {
|
|||||||
await createAdminUser(app.get(UsersService));
|
await createAdminUser(app.get(UsersService));
|
||||||
|
|
||||||
await app.listen(9494);
|
await app.listen(9494);
|
||||||
|
console.log(`Application is running on: ${await app.getUrl()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
27
backend/src/sources/plguin-loader.service.ts
Normal file
27
backend/src/sources/plguin-loader.service.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import axios from 'axios';
|
||||||
|
import * as vm from 'vm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PluginLoaderService {
|
||||||
|
constructor(@Inject('PLUGIN_URL') private readonly pluginUrl: string) {}
|
||||||
|
|
||||||
|
async loadPlugin(): Promise<any> {
|
||||||
|
const response = await axios.get(this.pluginUrl);
|
||||||
|
|
||||||
|
const sandbox = {
|
||||||
|
module: { exports: {} },
|
||||||
|
console: console,
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.createContext(sandbox);
|
||||||
|
|
||||||
|
const script = new vm.Script(response.data, {
|
||||||
|
filename: 'plugin-module.js',
|
||||||
|
});
|
||||||
|
script.runInContext(sandbox, { displayErrors: true });
|
||||||
|
|
||||||
|
return sandbox.module.exports;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
backend/src/sources/source-plugins.service.ts
Normal file
59
backend/src/sources/source-plugins.service.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
export interface SourcePlugin {
|
||||||
|
handleProxy(request: { uri: string; headers: any }): any;
|
||||||
|
name: string;
|
||||||
|
indexable: boolean;
|
||||||
|
getMovieStream: (tmdbId: string) => Promise<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SourcePluginsService {
|
||||||
|
private plugins: Record<string, SourcePlugin>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
console.log('Loading source plugins...');
|
||||||
|
|
||||||
|
this.plugins = this.loadPlugins(
|
||||||
|
path.join(require.main.path, '..', 'plugins'),
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Loaded source plugins: ${Object.keys(this.plugins).join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLoadedPlugins(): Promise<Record<string, SourcePlugin>> {
|
||||||
|
return this.plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadPlugins(rootDirectory: string): Record<string, SourcePlugin> {
|
||||||
|
const pluginDirectories = fs.readdirSync(rootDirectory);
|
||||||
|
|
||||||
|
const pluginPaths = [];
|
||||||
|
for (const directoryName of pluginDirectories) {
|
||||||
|
const directoryPath = path.join(rootDirectory, directoryName);
|
||||||
|
const directoryStat = fs.statSync(directoryPath);
|
||||||
|
|
||||||
|
if (directoryStat.isDirectory()) {
|
||||||
|
pluginPaths.push(directoryPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugins: Record<string, SourcePlugin> = {};
|
||||||
|
|
||||||
|
for (const pluginPath of pluginPaths) {
|
||||||
|
const pluginModule = require(pluginPath);
|
||||||
|
const plugin = new pluginModule.default();
|
||||||
|
plugins[plugin.name] = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPlugin(pluginName: string): SourcePlugin | undefined {
|
||||||
|
return this.plugins[pluginName];
|
||||||
|
}
|
||||||
|
}
|
||||||
68
backend/src/sources/sources.controller.ts
Normal file
68
backend/src/sources/sources.controller.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
All,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Next,
|
||||||
|
Param,
|
||||||
|
Req,
|
||||||
|
Res,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { SourcePluginsService } from './source-plugins.service';
|
||||||
|
import { AuthGuard } from 'src/auth/auth.guard';
|
||||||
|
import { NextFunction, Request, Response } from 'express';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
apiKey: '',
|
||||||
|
baseUrl: 'http://192.168.0.129:8096',
|
||||||
|
userId: '',
|
||||||
|
};
|
||||||
|
export const JELLYFIN_DEVICE_ID = 'Reiverr Client';
|
||||||
|
|
||||||
|
@ApiTags('sources')
|
||||||
|
@Controller('sources')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
export class SourcesController {
|
||||||
|
constructor(private sourcesService: SourcePluginsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async getSources() {
|
||||||
|
this.sourcesService.getLoadedPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':sourceId/movies/:tmdbId/stream')
|
||||||
|
async getMovieStream(
|
||||||
|
@Param('sourceId') sourceId: string,
|
||||||
|
@Param('tmdbId') tmdbId: string,
|
||||||
|
) {
|
||||||
|
return this.sourcesService.getPlugin(sourceId)?.getMovieStream(tmdbId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@All(':sourceId/movies/:tmdbId/stream/*')
|
||||||
|
async getMovieStreamProxy(
|
||||||
|
@Param() params: any,
|
||||||
|
@Req() req: Request,
|
||||||
|
@Res() res: Response,
|
||||||
|
@Next() next: NextFunction,
|
||||||
|
) {
|
||||||
|
const { url, headers } = this.sourcesService
|
||||||
|
.getPlugin(params.sourceId)
|
||||||
|
?.handleProxy({
|
||||||
|
uri: params[0] + '?' + req.url.split('?')[1],
|
||||||
|
headers: req.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
const proxyRes = await fetch(url, {
|
||||||
|
method: req.method || 'GET',
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${config.apiKey}"`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Readable.from(proxyRes.body).pipe(res);
|
||||||
|
res.status(proxyRes.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,14 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { DynamicModule } from '@nestjs/common';
|
||||||
|
import { PluginLoaderService } from './plguin-loader.service';
|
||||||
|
import { SourcePluginsService } from './source-plugins.service';
|
||||||
|
import { SourcesController } from './sources.controller';
|
||||||
|
import { UsersModule } from 'src/users/users.module';
|
||||||
|
|
||||||
@Module({})
|
@Module({
|
||||||
export class SourcesModule {}
|
providers: [SourcePluginsService],
|
||||||
|
controllers: [SourcesController],
|
||||||
|
exports: [SourcePluginsService],
|
||||||
|
imports: [UsersModule],
|
||||||
|
})
|
||||||
|
export class SourceModule {}
|
||||||
|
|||||||
@@ -18,5 +18,5 @@
|
|||||||
"forceConsistentCasingInFileNames": false,
|
"forceConsistentCasingInFileNames": false,
|
||||||
"noFallthroughCasesInSwitch": false,
|
"noFallthroughCasesInSwitch": false,
|
||||||
"lib": ["es6"]
|
"lib": ["es6"]
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
index.html
12
index.html
@@ -3,16 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="./public/favicon.png" />
|
<link rel="icon" href="./public/favicon.png" />
|
||||||
<meta
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
|
||||||
name="viewport"
|
|
||||||
content="width=device-width, initial-scale=1.0, user-scalable=no"
|
|
||||||
/>
|
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<meta
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
name="apple-mobile-web-app-status-bar-style"
|
|
||||||
content="black-translucent"
|
|
||||||
/>
|
|
||||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||||
<title>Vite + Svelte + TS</title>
|
<title>Vite + Svelte + TS</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
@@ -27,7 +21,7 @@
|
|||||||
/>
|
/>
|
||||||
<script>
|
<script>
|
||||||
document.documentElement.setAttribute('data-useragent', navigator.userAgent);
|
document.documentElement.setAttribute('data-useragent', navigator.userAgent);
|
||||||
document.documentElement.setAttribute('data-platform', navigator.platform );
|
document.documentElement.setAttribute('data-platform', navigator.platform);
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-secondary-900 min-h-screen text-white touch-manipulation relative -z-10">
|
<body class="bg-secondary-900 min-h-screen text-white touch-manipulation relative -z-10">
|
||||||
|
|||||||
20
package.json
20
package.json
@@ -71,5 +71,25 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gsap": "^3.12.5"
|
"gsap": "^3.12.5"
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"useTabs": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "none",
|
||||||
|
"printWidth": 100,
|
||||||
|
"plugins": [
|
||||||
|
"prettier-plugin-svelte"
|
||||||
|
],
|
||||||
|
"pluginSearchDirs": [
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": "*.svelte",
|
||||||
|
"options": {
|
||||||
|
"parser": "svelte"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
0
postcss.config.js
Normal file → Executable file
0
postcss.config.js
Normal file → Executable file
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Reiverr",
|
"name": "Reiverr",
|
||||||
"icons": [
|
"icons": [],
|
||||||
],
|
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#000000",
|
||||||
"background_color": "#000000",
|
"background_color": "#000000",
|
||||||
|
|||||||
13
src/app.css
13
src/app.css
@@ -38,11 +38,11 @@ a {
|
|||||||
/* @apply outline-offset-[2px]*/
|
/* @apply outline-offset-[2px]*/
|
||||||
/*}*/
|
/*}*/
|
||||||
|
|
||||||
html:not([data-useragent*="Tizen"]) .selectable {
|
html:not([data-useragent*='Tizen']) .selectable {
|
||||||
@apply focus:border-primary-500 focus-visible:border-primary-500;
|
@apply focus:border-primary-500 focus-visible:border-primary-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-useragent*="Tizen"] .selectable {
|
html[data-useragent*='Tizen'] .selectable {
|
||||||
@apply focus:border-primary-500 focus-within:border-primary-500;
|
@apply focus:border-primary-500 focus-within:border-primary-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,21 +50,20 @@ html[data-useragent*="Tizen"] .selectable {
|
|||||||
@apply outline-none outline-0 border-2 border-[#00000000] transition-colors hover:border-primary-500;
|
@apply outline-none outline-0 border-2 border-[#00000000] transition-colors hover:border-primary-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.selectable:focus, .selectable:focus-within {
|
.selectable:focus,
|
||||||
|
.selectable:focus-within {
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.selectable-secondary {
|
.selectable-secondary {
|
||||||
@apply outline-none outline-0 border-2 transition-colors hover:border-primary-500;
|
@apply outline-none outline-0 border-2 transition-colors hover:border-primary-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
html:not([data-useragent*="Tizen"]) .selectable-secondary {
|
html:not([data-useragent*='Tizen']) .selectable-secondary {
|
||||||
@apply focus-visible:border-primary-500;
|
@apply focus-visible:border-primary-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-useragent*="Tizen"] .selectable-secondary {
|
html[data-useragent*='Tizen'] .selectable-secondary {
|
||||||
@apply focus-within:border-primary-500;
|
@apply focus-within:border-primary-500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ import {
|
|||||||
/**
|
/**
|
||||||
* List of supported Ts audio codecs
|
* List of supported Ts audio codecs
|
||||||
*/
|
*/
|
||||||
export function getSupportedTsAudioCodecs(
|
export function getSupportedTsAudioCodecs(videoTestElement: HTMLVideoElement): string[] {
|
||||||
videoTestElement: HTMLVideoElement
|
|
||||||
): string[] {
|
|
||||||
const codecs = [];
|
const codecs = [];
|
||||||
|
|
||||||
if (hasAacSupport(videoTestElement)) {
|
if (hasAacSupport(videoTestElement)) {
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import { hasH264Support } from './mp4-video-formats';
|
|||||||
/**
|
/**
|
||||||
* List of supported ts video codecs
|
* List of supported ts video codecs
|
||||||
*/
|
*/
|
||||||
export function getSupportedTsVideoCodecs(
|
export function getSupportedTsVideoCodecs(videoTestElement: HTMLVideoElement): string[] {
|
||||||
videoTestElement: HTMLVideoElement
|
|
||||||
): string[] {
|
|
||||||
const codecs = [];
|
const codecs = [];
|
||||||
|
|
||||||
if (hasH264Support(videoTestElement)) {
|
if (hasH264Support(videoTestElement)) {
|
||||||
|
|||||||
@@ -2,18 +2,12 @@
|
|||||||
* @deprecated - Check @/utils/playback-profiles/index
|
* @deprecated - Check @/utils/playback-profiles/index
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { hasAv1Support, hasVp8Support, hasVp9Support } from './mp4-video-formats';
|
||||||
hasAv1Support,
|
|
||||||
hasVp8Support,
|
|
||||||
hasVp9Support
|
|
||||||
} from './mp4-video-formats';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an array of supported codecs WebM video codecs
|
* Get an array of supported codecs WebM video codecs
|
||||||
*/
|
*/
|
||||||
export function getSupportedWebMVideoCodecs(
|
export function getSupportedWebMVideoCodecs(videoTestElement: HTMLVideoElement): string[] {
|
||||||
videoTestElement: HTMLVideoElement
|
|
||||||
): string[] {
|
|
||||||
const codecs = [];
|
const codecs = [];
|
||||||
|
|
||||||
if (hasVp8Support(videoTestElement)) {
|
if (hasVp8Support(videoTestElement)) {
|
||||||
|
|||||||
@@ -199,22 +199,14 @@ export function isWebOS(): boolean {
|
|||||||
* Determines if current platform is WebOS1
|
* Determines if current platform is WebOS1
|
||||||
*/
|
*/
|
||||||
export function isWebOS1(): boolean {
|
export function isWebOS1(): boolean {
|
||||||
return (
|
return isWebOS() && userAgentContains('AppleWebKit/537') && !userAgentContains('Chrome/');
|
||||||
isWebOS() &&
|
|
||||||
userAgentContains('AppleWebKit/537') &&
|
|
||||||
!userAgentContains('Chrome/')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if current platform is WebOS2
|
* Determines if current platform is WebOS2
|
||||||
*/
|
*/
|
||||||
export function isWebOS2(): boolean {
|
export function isWebOS2(): boolean {
|
||||||
return (
|
return isWebOS() && userAgentContains('AppleWebKit/538') && !userAgentContains('Chrome/');
|
||||||
isWebOS() &&
|
|
||||||
userAgentContains('AppleWebKit/538') &&
|
|
||||||
!userAgentContains('Chrome/')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
0
svelte.config.js
Normal file → Executable file
0
svelte.config.js
Normal file → Executable file
File diff suppressed because one or more lines are too long
0
tailwind.config.js
Normal file → Executable file
0
tailwind.config.js
Normal file → Executable file
@@ -1,18 +1,16 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
|
||||||
<meta name="description" content="Tizen basic template generated by Tizen Web IDE"/>
|
<meta name="description" content="Tizen basic template generated by Tizen Web IDE" />
|
||||||
|
|
||||||
<title>Tizen Web IDE - Tizen - Samsung Tizen TV basic Application</title>
|
<title>Tizen Web IDE - Tizen - Samsung Tizen TV basic Application</title>
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="css/style.css"/>
|
<link rel="stylesheet" type="text/css" href="css/style.css" />
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
<meta http-equiv="refresh" content="0; url=dist/index.html" />
|
<meta http-equiv="refresh" content="0; url=dist/index.html" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body></body>
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noUncheckedIndexedAccess": true
|
"noUncheckedIndexedAccess": true
|
||||||
// "paths": {
|
// "paths": {
|
||||||
// "$lib/*": ["src/lib/*"],
|
// "$lib/*": ["src/lib/*"],
|
||||||
// }
|
// }
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
|||||||
Reference in New Issue
Block a user