mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 04:08:45 -05:00
Merge remote-tracking branch 'upstream/develop' into fix/jellyfin-translation
This commit is contained in:
@@ -872,6 +872,33 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "scorp200",
|
||||||
|
"name": "Anton K. (ai Doge)",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/9427639?v=4",
|
||||||
|
"profile": "http://aidoge.xyz",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "marcofaggian",
|
||||||
|
"name": "Marco Faggian",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/19221001?v=4",
|
||||||
|
"profile": "https://marcofaggian.com",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "nemchik",
|
||||||
|
"name": "Eric Nemchik",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/725456?v=4",
|
||||||
|
"profile": "http://nemchik.com/",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
"badgeTemplate": "<a href=\"#contributors-\"><img alt=\"All Contributors\" src=\"https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg\"/></a>",
|
||||||
|
|||||||
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
@@ -31,7 +31,7 @@ jobs:
|
|||||||
build_and_push:
|
build_and_push:
|
||||||
name: Build & Publish Docker Images
|
name: Build & Publish Docker Images
|
||||||
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
|
if: github.ref == 'refs/heads/develop' && !contains(github.event.head_commit.message, '[skip ci]')
|
||||||
runs-on: self-hosted
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@@ -39,13 +39,6 @@ jobs:
|
|||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: Cache Docker layers
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: /tmp/.buildx-cache
|
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-buildx-
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
@@ -68,15 +61,6 @@ jobs:
|
|||||||
COMMIT_TAG=${{ github.sha }}
|
COMMIT_TAG=${{ github.sha }}
|
||||||
tags: |
|
tags: |
|
||||||
fallenbagel/jellyseerr:develop
|
fallenbagel/jellyseerr:develop
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
|
||||||
- # Temporary fix
|
|
||||||
# https://github.com/docker/build-push-action/issues/252
|
|
||||||
# https://github.com/moby/buildkit/issues/1896
|
|
||||||
name: Move cache
|
|
||||||
run: |
|
|
||||||
rm -rf /tmp/.buildx-cache
|
|
||||||
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
|
||||||
|
|
||||||
discord:
|
discord:
|
||||||
name: Send Discord Notification
|
name: Send Discord Notification
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
|
<a href="https://discord.gg/ckbvBtDJgC"><img src="https://img.shields.io/badge/Discord-Chat-lightgrey" alt="Discord"></a>
|
||||||
<a href="https://hub.docker.com/r/fallenbagel/jellyseerr"><img src="https://img.shields.io/docker/pulls/fallenbagel/jellyseerr" alt="Docker pulls"></a>
|
<a href="https://hub.docker.com/r/fallenbagel/jellyseerr"><img src="https://img.shields.io/docker/pulls/fallenbagel/jellyseerr" alt="Docker pulls"></a>
|
||||||
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
|
<a href="https://github.com/fallenbagel/jellyseerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/fallenbagel/jellyseerr"></a>
|
||||||
|
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||||
|
<a href="#contributors-"><img alt="All Contributors" src="https://img.shields.io/badge/all_contributors-98-orange.svg"/></a>
|
||||||
|
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||||
|
|
||||||
**Jellyseerr** is a free and open source software application for managing requests for your media library. It is a a fork of Overseerr built to bring support for Jellyfin & Emby media servers!
|
**Jellyseerr** is a free and open source software application for managing requests for your media library. It is a a fork of Overseerr built to bring support for Jellyfin & Emby media servers!
|
||||||
|
|
||||||
|
|||||||
@@ -5657,6 +5657,63 @@ paths:
|
|||||||
audienceRating:
|
audienceRating:
|
||||||
type: string
|
type: string
|
||||||
enum: ['Spilled', 'Upright']
|
enum: ['Spilled', 'Upright']
|
||||||
|
/movie/{movieId}/ratingscombined:
|
||||||
|
get:
|
||||||
|
summary: Get RT and IMDB movie ratings combined
|
||||||
|
description: Returns ratings from RottenTomatoes and IMDB based on the provided movieId in a JSON object.
|
||||||
|
tags:
|
||||||
|
- movies
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: movieId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: number
|
||||||
|
example: 337401
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Ratings returned
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
rt:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
example: Mulan
|
||||||
|
year:
|
||||||
|
type: number
|
||||||
|
example: 2020
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
example: 'http://www.rottentomatoes.com/m/mulan_2020/'
|
||||||
|
criticsScore:
|
||||||
|
type: number
|
||||||
|
example: 85
|
||||||
|
criticsRating:
|
||||||
|
type: string
|
||||||
|
enum: ['Rotten', 'Fresh', 'Certified Fresh']
|
||||||
|
audienceScore:
|
||||||
|
type: number
|
||||||
|
example: 65
|
||||||
|
audienceRating:
|
||||||
|
type: string
|
||||||
|
enum: ['Spilled', 'Upright']
|
||||||
|
imdb:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
example: I am Legend
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
example: 'https://www.imdb.com/title/tt0480249'
|
||||||
|
criticsScore:
|
||||||
|
type: number
|
||||||
|
example: 6.5
|
||||||
/tv/{tvId}:
|
/tv/{tvId}:
|
||||||
get:
|
get:
|
||||||
summary: Get TV details
|
summary: Get TV details
|
||||||
|
|||||||
@@ -171,23 +171,20 @@ class JellyfinAPI {
|
|||||||
|
|
||||||
public async getLibraries(): Promise<JellyfinLibrary[]> {
|
public async getLibraries(): Promise<JellyfinLibrary[]> {
|
||||||
try {
|
try {
|
||||||
const account = await this.axios.get<any>(
|
const libraries = await this.axios.get<any>('/Library/VirtualFolders');
|
||||||
`/Users/${this.userId ?? 'Me'}/Views`
|
|
||||||
);
|
|
||||||
|
|
||||||
const response: JellyfinLibrary[] = account.data.Items.filter(
|
const response: JellyfinLibrary[] = libraries.data
|
||||||
(Item: any) => {
|
.filter((Item: any) => {
|
||||||
return (
|
return (
|
||||||
Item.Type === 'CollectionFolder' &&
|
|
||||||
Item.CollectionType !== 'music' &&
|
Item.CollectionType !== 'music' &&
|
||||||
Item.CollectionType !== 'books' &&
|
Item.CollectionType !== 'books' &&
|
||||||
Item.CollectionType !== 'musicvideos' &&
|
Item.CollectionType !== 'musicvideos' &&
|
||||||
Item.CollectionType !== 'homevideos'
|
Item.CollectionType !== 'homevideos'
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
).map((Item: any) => {
|
.map((Item: any) => {
|
||||||
return <JellyfinLibrary>{
|
return <JellyfinLibrary>{
|
||||||
key: Item.Id,
|
key: Item.ItemId,
|
||||||
title: Item.Name,
|
title: Item.Name,
|
||||||
type: Item.CollectionType === 'movies' ? 'movie' : 'show',
|
type: Item.CollectionType === 'movies' ? 'movie' : 'show',
|
||||||
agent: 'jellyfin',
|
agent: 'jellyfin',
|
||||||
|
|||||||
@@ -82,21 +82,6 @@ interface ServerResponse {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FriendResponse {
|
|
||||||
MediaContainer: {
|
|
||||||
User: {
|
|
||||||
$: {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
thumb: string;
|
|
||||||
};
|
|
||||||
Server?: ServerResponse[];
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UsersResponse {
|
interface UsersResponse {
|
||||||
MediaContainer: {
|
MediaContainer: {
|
||||||
User: {
|
User: {
|
||||||
@@ -234,19 +219,6 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFriends(): Promise<FriendResponse> {
|
|
||||||
const response = await this.axios.get('/pms/friends/all', {
|
|
||||||
transformResponse: [],
|
|
||||||
responseType: 'text',
|
|
||||||
});
|
|
||||||
|
|
||||||
const parsedXml = (await xml2js.parseStringPromise(
|
|
||||||
response.data
|
|
||||||
)) as FriendResponse;
|
|
||||||
|
|
||||||
return parsedXml;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async checkUserAccess(userId: number): Promise<boolean> {
|
public async checkUserAccess(userId: number): Promise<boolean> {
|
||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
@@ -255,9 +227,9 @@ class PlexTvAPI extends ExternalAPI {
|
|||||||
throw new Error('Plex is not configured!');
|
throw new Error('Plex is not configured!');
|
||||||
}
|
}
|
||||||
|
|
||||||
const friends = await this.getFriends();
|
const usersResponse = await this.getUsers();
|
||||||
|
|
||||||
const users = friends.MediaContainer.User;
|
const users = usersResponse.MediaContainer.User;
|
||||||
|
|
||||||
const user = users.find((u) => parseInt(u.$.id) === userId);
|
const user = users.find((u) => parseInt(u.$.id) === userId);
|
||||||
|
|
||||||
|
|||||||
195
server/api/rating/imdbRadarrProxy.ts
Normal file
195
server/api/rating/imdbRadarrProxy.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
|
import cacheManager from '@server/lib/cache';
|
||||||
|
|
||||||
|
type IMDBRadarrProxyResponse = IMDBMovie[];
|
||||||
|
|
||||||
|
interface IMDBMovie {
|
||||||
|
ImdbId: string;
|
||||||
|
Overview: string;
|
||||||
|
Title: string;
|
||||||
|
OriginalTitle: string;
|
||||||
|
TitleSlug: string;
|
||||||
|
Ratings: Rating[];
|
||||||
|
MovieRatings: MovieRatings;
|
||||||
|
Runtime: number;
|
||||||
|
Images: Image[];
|
||||||
|
Genres: string[];
|
||||||
|
Popularity: number;
|
||||||
|
Premier: string;
|
||||||
|
InCinema: string;
|
||||||
|
PhysicalRelease: any;
|
||||||
|
DigitalRelease: string;
|
||||||
|
Year: number;
|
||||||
|
AlternativeTitles: AlternativeTitle[];
|
||||||
|
Translations: Translation[];
|
||||||
|
Recommendations: Recommendation[];
|
||||||
|
Credits: Credits;
|
||||||
|
Studio: string;
|
||||||
|
YoutubeTrailerId: string;
|
||||||
|
Certifications: Certification[];
|
||||||
|
Status: any;
|
||||||
|
Collection: Collection;
|
||||||
|
OriginalLanguage: string;
|
||||||
|
Homepage: string;
|
||||||
|
TmdbId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Rating {
|
||||||
|
Count: number;
|
||||||
|
Value: number;
|
||||||
|
Origin: string;
|
||||||
|
Type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MovieRatings {
|
||||||
|
Tmdb: Tmdb;
|
||||||
|
Imdb: Imdb;
|
||||||
|
Metacritic: Metacritic;
|
||||||
|
RottenTomatoes: RottenTomatoes;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tmdb {
|
||||||
|
Count: number;
|
||||||
|
Value: number;
|
||||||
|
Type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Imdb {
|
||||||
|
Count: number;
|
||||||
|
Value: number;
|
||||||
|
Type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Metacritic {
|
||||||
|
Count: number;
|
||||||
|
Value: number;
|
||||||
|
Type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RottenTomatoes {
|
||||||
|
Count: number;
|
||||||
|
Value: number;
|
||||||
|
Type: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Image {
|
||||||
|
CoverType: string;
|
||||||
|
Url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AlternativeTitle {
|
||||||
|
Title: string;
|
||||||
|
Type: string;
|
||||||
|
Language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Translation {
|
||||||
|
Title: string;
|
||||||
|
Overview: string;
|
||||||
|
Language: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Recommendation {
|
||||||
|
TmdbId: number;
|
||||||
|
Title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Credits {
|
||||||
|
Cast: Cast[];
|
||||||
|
Crew: Crew[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Cast {
|
||||||
|
Name: string;
|
||||||
|
Order: number;
|
||||||
|
Character: string;
|
||||||
|
TmdbId: number;
|
||||||
|
CreditId: string;
|
||||||
|
Images: Image2[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Image2 {
|
||||||
|
CoverType: string;
|
||||||
|
Url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Crew {
|
||||||
|
Name: string;
|
||||||
|
Job: string;
|
||||||
|
Department: string;
|
||||||
|
TmdbId: number;
|
||||||
|
CreditId: string;
|
||||||
|
Images: Image3[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Image3 {
|
||||||
|
CoverType: string;
|
||||||
|
Url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Certification {
|
||||||
|
Country: string;
|
||||||
|
Certification: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Collection {
|
||||||
|
Name: string;
|
||||||
|
Images: any;
|
||||||
|
Overview: any;
|
||||||
|
Translations: any;
|
||||||
|
Parts: any;
|
||||||
|
TmdbId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IMDBRating {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
criticsScore: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is a best-effort API. The IMDB API is technically
|
||||||
|
* private and getting access costs money/requires approval.
|
||||||
|
*
|
||||||
|
* Radarr hosts a public proxy that's in use by all Radarr instances.
|
||||||
|
*/
|
||||||
|
class IMDBRadarrProxy extends ExternalAPI {
|
||||||
|
constructor() {
|
||||||
|
super('https://api.radarr.video/v1', {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
nodeCache: cacheManager.getCache('imdb').data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ask the Radarr IMDB Proxy for the movie
|
||||||
|
*
|
||||||
|
* @param IMDBid Id of IMDB movie
|
||||||
|
*/
|
||||||
|
public async getMovieRatings(IMDBid: string): Promise<IMDBRating | null> {
|
||||||
|
try {
|
||||||
|
const data = await this.get<IMDBRadarrProxyResponse>(
|
||||||
|
`/movie/imdb/${IMDBid}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!data?.length || data[0].ImdbId !== IMDBid) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: data[0].Title,
|
||||||
|
url: `https://www.imdb.com/title/${data[0].ImdbId}`,
|
||||||
|
criticsScore: data[0].MovieRatings.Imdb.Value,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(
|
||||||
|
`[IMDB RADARR PROXY API] Failed to retrieve movie ratings: ${e.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IMDBRadarrProxy;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
import cacheManager from '@server/lib/cache';
|
import cacheManager from '@server/lib/cache';
|
||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import ExternalAPI from './externalapi';
|
|
||||||
|
|
||||||
interface RTAlgoliaSearchResponse {
|
interface RTAlgoliaSearchResponse {
|
||||||
results: {
|
results: {
|
||||||
@@ -144,6 +144,9 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
? 'Fresh'
|
? 'Fresh'
|
||||||
: 'Rotten',
|
: 'Rotten',
|
||||||
criticsScore: movie.rottenTomatoes.criticsScore,
|
criticsScore: movie.rottenTomatoes.criticsScore,
|
||||||
|
audienceRating:
|
||||||
|
movie.rottenTomatoes.audienceScore >= 60 ? 'Upright' : 'Spilled',
|
||||||
|
audienceScore: movie.rottenTomatoes.audienceScore,
|
||||||
year: Number(movie.releaseYear),
|
year: Number(movie.releaseYear),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -192,6 +195,9 @@ class RottenTomatoes extends ExternalAPI {
|
|||||||
criticsRating:
|
criticsRating:
|
||||||
tvshow.rottenTomatoes.criticsScore >= 60 ? 'Fresh' : 'Rotten',
|
tvshow.rottenTomatoes.criticsScore >= 60 ? 'Fresh' : 'Rotten',
|
||||||
criticsScore: tvshow.rottenTomatoes.criticsScore,
|
criticsScore: tvshow.rottenTomatoes.criticsScore,
|
||||||
|
audienceRating:
|
||||||
|
tvshow.rottenTomatoes.audienceScore >= 60 ? 'Upright' : 'Spilled',
|
||||||
|
audienceScore: tvshow.rottenTomatoes.audienceScore,
|
||||||
year: Number(tvshow.releaseYear),
|
year: Number(tvshow.releaseYear),
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
7
server/api/ratings.ts
Normal file
7
server/api/ratings.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { type IMDBRating } from '@server/api/rating/imdbRadarrProxy';
|
||||||
|
import { type RTRating } from '@server/api/rating/rottentomatoes';
|
||||||
|
|
||||||
|
export interface RatingResponse {
|
||||||
|
rt?: RTRating;
|
||||||
|
imdb?: IMDBRating;
|
||||||
|
}
|
||||||
@@ -311,13 +311,15 @@ class JobJellyfinSync {
|
|||||||
// setting the status to AVAILABLE if all of a type is there, partially if some,
|
// setting the status to AVAILABLE if all of a type is there, partially if some,
|
||||||
// and then not modifying the status if there are 0 items
|
// and then not modifying the status if there are 0 items
|
||||||
existingSeason.status =
|
existingSeason.status =
|
||||||
totalStandard >= season.episode_count
|
totalStandard >= season.episode_count ||
|
||||||
|
existingSeason.status === MediaStatus.AVAILABLE
|
||||||
? MediaStatus.AVAILABLE
|
? MediaStatus.AVAILABLE
|
||||||
: totalStandard > 0
|
: totalStandard > 0
|
||||||
? MediaStatus.PARTIALLY_AVAILABLE
|
? MediaStatus.PARTIALLY_AVAILABLE
|
||||||
: existingSeason.status;
|
: existingSeason.status;
|
||||||
existingSeason.status4k =
|
existingSeason.status4k =
|
||||||
this.enable4kShow && total4k >= season.episode_count
|
(this.enable4kShow && total4k >= season.episode_count) ||
|
||||||
|
existingSeason.status4k === MediaStatus.AVAILABLE
|
||||||
? MediaStatus.AVAILABLE
|
? MediaStatus.AVAILABLE
|
||||||
: this.enable4kShow && total4k > 0
|
: this.enable4kShow && total4k > 0
|
||||||
? MediaStatus.PARTIALLY_AVAILABLE
|
? MediaStatus.PARTIALLY_AVAILABLE
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { JobId } from '@server/lib/settings';
|
|||||||
import { getSettings } from '@server/lib/settings';
|
import { getSettings } from '@server/lib/settings';
|
||||||
import watchlistSync from '@server/lib/watchlistsync';
|
import watchlistSync from '@server/lib/watchlistsync';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
|
import random from 'lodash/random';
|
||||||
import schedule from 'node-schedule';
|
import schedule from 'node-schedule';
|
||||||
import { jobJellyfinFullSync, jobJellyfinRecentSync } from './jellyfinsync';
|
import { jobJellyfinFullSync, jobJellyfinRecentSync } from './jellyfinsync';
|
||||||
|
|
||||||
@@ -107,21 +108,31 @@ export const startJobs = (): void => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run watchlist sync every 5 minutes
|
// Watchlist Sync
|
||||||
scheduledJobs.push({
|
const watchlistSyncJob: ScheduledJob = {
|
||||||
id: 'plex-watchlist-sync',
|
id: 'plex-watchlist-sync',
|
||||||
name: 'Plex Watchlist Sync',
|
name: 'Plex Watchlist Sync',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
interval: 'minutes',
|
interval: 'fixed',
|
||||||
cronSchedule: jobs['plex-watchlist-sync'].schedule,
|
cronSchedule: jobs['plex-watchlist-sync'].schedule,
|
||||||
job: schedule.scheduleJob(jobs['plex-watchlist-sync'].schedule, () => {
|
job: schedule.scheduleJob(new Date(Date.now() + 1000 * 60 * 20), () => {
|
||||||
logger.info('Starting scheduled job: Plex Watchlist Sync', {
|
logger.info('Starting scheduled job: Plex Watchlist Sync', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
});
|
});
|
||||||
watchlistSync.syncWatchlist();
|
watchlistSync.syncWatchlist();
|
||||||
}),
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// To help alleviate load on Plex's servers, we will add some fuzziness to the next schedule
|
||||||
|
// after each run
|
||||||
|
watchlistSyncJob.job.on('run', () => {
|
||||||
|
watchlistSyncJob.job.schedule(
|
||||||
|
new Date(Math.floor(Date.now() + 1000 * 60 * random(14, 24, true)))
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
scheduledJobs.push(watchlistSyncJob);
|
||||||
|
|
||||||
// Run full radarr scan every 24 hours
|
// Run full radarr scan every 24 hours
|
||||||
scheduledJobs.push({
|
scheduledJobs.push({
|
||||||
id: 'radarr-scan',
|
id: 'radarr-scan',
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ export type AvailableCacheIds =
|
|||||||
| 'radarr'
|
| 'radarr'
|
||||||
| 'sonarr'
|
| 'sonarr'
|
||||||
| 'rt'
|
| 'rt'
|
||||||
|
| 'imdb'
|
||||||
| 'github'
|
| 'github'
|
||||||
| 'plexguid'
|
| 'plexguid'
|
||||||
| 'plextv';
|
| 'plextv';
|
||||||
@@ -51,6 +52,10 @@ class CacheManager {
|
|||||||
stdTtl: 43200,
|
stdTtl: 43200,
|
||||||
checkPeriod: 60 * 30,
|
checkPeriod: 60 * 30,
|
||||||
}),
|
}),
|
||||||
|
imdb: new Cache('imdb', 'IMDB Radarr Proxy', {
|
||||||
|
stdTtl: 43200,
|
||||||
|
checkPeriod: 60 * 30,
|
||||||
|
}),
|
||||||
github: new Cache('github', 'GitHub API', {
|
github: new Cache('github', 'GitHub API', {
|
||||||
stdTtl: 21600,
|
stdTtl: 21600,
|
||||||
checkPeriod: 60 * 30,
|
checkPeriod: 60 * 30,
|
||||||
|
|||||||
@@ -404,7 +404,7 @@ class Settings {
|
|||||||
options: {
|
options: {
|
||||||
webhookUrl: '',
|
webhookUrl: '',
|
||||||
jsonPayload:
|
jsonPayload:
|
||||||
'IntcbiAgICBcIm5vdGlmaWNhdGlvbl90eXBlXCI6IFwie3tub3RpZmljYXRpb25fdHlwZX19XCIsXG4gICAgXCJldmVudFwiOiBcInt7ZXZlbnR9fVwiLFxuICAgIFwic3ViamVjdFwiOiBcInt7c3ViamVjdH19XCIsXG4gICAgXCJtZXNzYWdlXCI6IFwie3ttZXNzYWdlfX1cIixcbiAgICBcImltYWdlXCI6IFwie3tpbWFnZX19XCIsXG4gICAgXCJ7e21lZGlhfX1cIjoge1xuICAgICAgICBcIm1lZGlhX3R5cGVcIjogXCJ7e21lZGlhX3R5cGV9fVwiLFxuICAgICAgICBcInRtZGJJZFwiOiBcInt7bWVkaWFfdG1kYmlkfX1cIixcbiAgICAgICAgXCJ0dmRiSWRcIjogXCJ7e21lZGlhX3R2ZGJpZH19XCIsXG4gICAgICAgIFwic3RhdHVzXCI6IFwie3ttZWRpYV9zdGF0dXN9fVwiLFxuICAgICAgICBcInN0YXR1czRrXCI6IFwie3ttZWRpYV9zdGF0dXM0a319XCJcbiAgICB9LFxuICAgIFwie3tyZXF1ZXN0fX1cIjoge1xuICAgICAgICBcInJlcXVlc3RfaWRcIjogXCJ7e3JlcXVlc3RfaWR9fVwiLFxuICAgICAgICBcInJlcXVlc3RlZEJ5X2VtYWlsXCI6IFwie3tyZXF1ZXN0ZWRCeV9lbWFpbH19XCIsXG4gICAgICAgIFwicmVxdWVzdGVkQnlfdXNlcm5hbWVcIjogXCJ7e3JlcXVlc3RlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXF1ZXN0ZWRCeV9hdmF0YXJcIjogXCJ7e3JlcXVlc3RlZEJ5X2F2YXRhcn19XCJcbiAgICB9LFxuICAgIFwie3tpc3N1ZX19XCI6IHtcbiAgICAgICAgXCJpc3N1ZV9pZFwiOiBcInt7aXNzdWVfaWR9fVwiLFxuICAgICAgICBcImlzc3VlX3R5cGVcIjogXCJ7e2lzc3VlX3R5cGV9fVwiLFxuICAgICAgICBcImlzc3VlX3N0YXR1c1wiOiBcInt7aXNzdWVfc3RhdHVzfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2VtYWlsXCI6IFwie3tyZXBvcnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X3VzZXJuYW1lXCI6IFwie3tyZXBvcnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICAgICAgXCJyZXBvcnRlZEJ5X2F2YXRhclwiOiBcInt7cmVwb3J0ZWRCeV9hdmF0YXJ9fVwiXG4gICAgfSxcbiAgICBcInt7Y29tbWVudH19XCI6IHtcbiAgICAgICAgXCJjb21tZW50X21lc3NhZ2VcIjogXCJ7e2NvbW1lbnRfbWVzc2FnZX19XCIsXG4gICAgICAgIFwiY29tbWVudGVkQnlfZW1haWxcIjogXCJ7e2NvbW1lbnRlZEJ5X2VtYWlsfX1cIixcbiAgICAgICAgXCJjb21tZW50ZWRCeV91c2VybmFtZVwiOiBcInt7Y29tbWVudGVkQnlfdXNlcm5hbWV9fVwiLFxuICAgICAgICBcImNvbW1lbnRlZEJ5X2F2YXRhclwiOiBcInt7Y29tbWVudGVkQnlfYXZhdGFyfX1cIlxuICAgIH0sXG4gICAgXCJ7e2V4dHJhfX1cIjogW11cbn0i',
|
'IntcbiAgXCJub3RpZmljYXRpb25fdHlwZVwiOiBcInt7bm90aWZpY2F0aW9uX3R5cGV9fVwiLFxuICBcImV2ZW50XCI6IFwie3tldmVudH19XCIsXG4gIFwic3ViamVjdFwiOiBcInt7c3ViamVjdH19XCIsXG4gIFwibWVzc2FnZVwiOiBcInt7bWVzc2FnZX19XCIsXG4gIFwiaW1hZ2VcIjogXCJ7e2ltYWdlfX1cIixcbiAgXCJ7e21lZGlhfX1cIjoge1xuICAgIFwibWVkaWFfdHlwZVwiOiBcInt7bWVkaWFfdHlwZX19XCIsXG4gICAgXCJ0bWRiSWRcIjogXCJ7e21lZGlhX3RtZGJpZH19XCIsXG4gICAgXCJ0dmRiSWRcIjogXCJ7e21lZGlhX3R2ZGJpZH19XCIsXG4gICAgXCJzdGF0dXNcIjogXCJ7e21lZGlhX3N0YXR1c319XCIsXG4gICAgXCJzdGF0dXM0a1wiOiBcInt7bWVkaWFfc3RhdHVzNGt9fVwiXG4gIH0sXG4gIFwie3tyZXF1ZXN0fX1cIjoge1xuICAgIFwicmVxdWVzdF9pZFwiOiBcInt7cmVxdWVzdF9pZH19XCIsXG4gICAgXCJyZXF1ZXN0ZWRCeV9lbWFpbFwiOiBcInt7cmVxdWVzdGVkQnlfZW1haWx9fVwiLFxuICAgIFwicmVxdWVzdGVkQnlfdXNlcm5hbWVcIjogXCJ7e3JlcXVlc3RlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICBcInJlcXVlc3RlZEJ5X2F2YXRhclwiOiBcInt7cmVxdWVzdGVkQnlfYXZhdGFyfX1cIixcbiAgICBcInJlcXVlc3RlZEJ5X3NldHRpbmdzX2Rpc2NvcmRJZFwiOiBcInt7cmVxdWVzdGVkQnlfc2V0dGluZ3NfZGlzY29yZElkfX1cIixcbiAgICBcInJlcXVlc3RlZEJ5X3NldHRpbmdzX3RlbGVncmFtQ2hhdElkXCI6IFwie3tyZXF1ZXN0ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZH19XCJcbiAgfSxcbiAgXCJ7e2lzc3VlfX1cIjoge1xuICAgIFwiaXNzdWVfaWRcIjogXCJ7e2lzc3VlX2lkfX1cIixcbiAgICBcImlzc3VlX3R5cGVcIjogXCJ7e2lzc3VlX3R5cGV9fVwiLFxuICAgIFwiaXNzdWVfc3RhdHVzXCI6IFwie3tpc3N1ZV9zdGF0dXN9fVwiLFxuICAgIFwicmVwb3J0ZWRCeV9lbWFpbFwiOiBcInt7cmVwb3J0ZWRCeV9lbWFpbH19XCIsXG4gICAgXCJyZXBvcnRlZEJ5X3VzZXJuYW1lXCI6IFwie3tyZXBvcnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICBcInJlcG9ydGVkQnlfYXZhdGFyXCI6IFwie3tyZXBvcnRlZEJ5X2F2YXRhcn19XCIsXG4gICAgXCJyZXBvcnRlZEJ5X3NldHRpbmdzX2Rpc2NvcmRJZFwiOiBcInt7cmVwb3J0ZWRCeV9zZXR0aW5nc19kaXNjb3JkSWR9fVwiLFxuICAgIFwicmVwb3J0ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZFwiOiBcInt7cmVwb3J0ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZH19XCJcbiAgfSxcbiAgXCJ7e2NvbW1lbnR9fVwiOiB7XG4gICAgXCJjb21tZW50X21lc3NhZ2VcIjogXCJ7e2NvbW1lbnRfbWVzc2FnZX19XCIsXG4gICAgXCJjb21tZW50ZWRCeV9lbWFpbFwiOiBcInt7Y29tbWVudGVkQnlfZW1haWx9fVwiLFxuICAgIFwiY29tbWVudGVkQnlfdXNlcm5hbWVcIjogXCJ7e2NvbW1lbnRlZEJ5X3VzZXJuYW1lfX1cIixcbiAgICBcImNvbW1lbnRlZEJ5X2F2YXRhclwiOiBcInt7Y29tbWVudGVkQnlfYXZhdGFyfX1cIixcbiAgICBcImNvbW1lbnRlZEJ5X3NldHRpbmdzX2Rpc2NvcmRJZFwiOiBcInt7Y29tbWVudGVkQnlfc2V0dGluZ3NfZGlzY29yZElkfX1cIixcbiAgICBcImNvbW1lbnRlZEJ5X3NldHRpbmdzX3RlbGVncmFtQ2hhdElkXCI6IFwie3tjb21tZW50ZWRCeV9zZXR0aW5nc190ZWxlZ3JhbUNoYXRJZH19XCJcbiAgfSxcbiAgXCJ7e2V4dHJhfX1cIjogW11cbn0i',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
webpush: {
|
webpush: {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import RottenTomatoes from '@server/api/rottentomatoes';
|
import IMDBRadarrProxy from '@server/api/rating/imdbRadarrProxy';
|
||||||
|
import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
||||||
|
import { type RatingResponse } from '@server/api/ratings';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
import { MediaType } from '@server/constants/media';
|
import { MediaType } from '@server/constants/media';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
@@ -118,6 +120,9 @@ movieRoutes.get('/:id/similar', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint backed by RottenTomatoes
|
||||||
|
*/
|
||||||
movieRoutes.get('/:id/ratings', async (req, res, next) => {
|
movieRoutes.get('/:id/ratings', async (req, res, next) => {
|
||||||
const tmdb = new TheMovieDb();
|
const tmdb = new TheMovieDb();
|
||||||
const rtapi = new RottenTomatoes();
|
const rtapi = new RottenTomatoes();
|
||||||
@@ -153,4 +158,53 @@ movieRoutes.get('/:id/ratings', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Endpoint combining RottenTomatoes and IMDB
|
||||||
|
*/
|
||||||
|
movieRoutes.get('/:id/ratingscombined', async (req, res, next) => {
|
||||||
|
const tmdb = new TheMovieDb();
|
||||||
|
const rtapi = new RottenTomatoes();
|
||||||
|
const imdbApi = new IMDBRadarrProxy();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const movie = await tmdb.getMovie({
|
||||||
|
movieId: Number(req.params.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
const rtratings = await rtapi.getMovieRatings(
|
||||||
|
movie.title,
|
||||||
|
Number(movie.release_date.slice(0, 4))
|
||||||
|
);
|
||||||
|
|
||||||
|
let imdbRatings;
|
||||||
|
if (movie.imdb_id) {
|
||||||
|
imdbRatings = await imdbApi.getMovieRatings(movie.imdb_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rtratings && !imdbRatings) {
|
||||||
|
return next({
|
||||||
|
status: 404,
|
||||||
|
message: 'No ratings found.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratings: RatingResponse = {
|
||||||
|
...(rtratings ? { rt: rtratings } : {}),
|
||||||
|
...(imdbRatings ? { imdb: imdbRatings } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.status(200).json(ratings);
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug('Something went wrong retrieving movie ratings', {
|
||||||
|
label: 'API',
|
||||||
|
errorMessage: e.message,
|
||||||
|
movieId: req.params.id,
|
||||||
|
});
|
||||||
|
return next({
|
||||||
|
status: 500,
|
||||||
|
message: 'Unable to retrieve movie ratings.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default movieRoutes;
|
export default movieRoutes;
|
||||||
|
|||||||
@@ -367,6 +367,7 @@ settingsRoutes.post('/tautulli', async (req, res, next) => {
|
|||||||
|
|
||||||
Object.assign(settings.tautulli, req.body);
|
Object.assign(settings.tautulli, req.body);
|
||||||
|
|
||||||
|
if (settings.tautulli.hostname) {
|
||||||
try {
|
try {
|
||||||
const tautulliClient = new TautulliAPI(settings.tautulli);
|
const tautulliClient = new TautulliAPI(settings.tautulli);
|
||||||
|
|
||||||
@@ -387,6 +388,7 @@ settingsRoutes.post('/tautulli', async (req, res, next) => {
|
|||||||
message: 'Unable to connect to Tautulli.',
|
message: 'Unable to connect to Tautulli.',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).json(settings.tautulli);
|
return res.status(200).json(settings.tautulli);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import RottenTomatoes from '@server/api/rottentomatoes';
|
import RottenTomatoes from '@server/api/rating/rottentomatoes';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
import { MediaType } from '@server/constants/media';
|
import { MediaType } from '@server/constants/media';
|
||||||
import Media from '@server/entity/Media';
|
import Media from '@server/entity/Media';
|
||||||
|
|||||||
@@ -72,9 +72,7 @@ const SidebarLinks: SidebarLinkProps[] = [
|
|||||||
{
|
{
|
||||||
href: '/issues',
|
href: '/issues',
|
||||||
messagesKey: 'issues',
|
messagesKey: 'issues',
|
||||||
svgIcon: (
|
svgIcon: <ExclamationTriangleIcon className="mr-3 h-6 w-6" />,
|
||||||
<ExclamationTriangleIcon className="mr-3 h-6 w-6 text-gray-300 transition duration-150 ease-in-out group-hover:text-gray-100 group-focus:text-gray-300" />
|
|
||||||
),
|
|
||||||
activeRegExp: /^\/issues/,
|
activeRegExp: /^\/issues/,
|
||||||
requiredPermission: [
|
requiredPermission: [
|
||||||
Permission.MANAGE_ISSUES,
|
Permission.MANAGE_ISSUES,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import RTAudFresh from '@app/assets/rt_aud_fresh.svg';
|
|||||||
import RTAudRotten from '@app/assets/rt_aud_rotten.svg';
|
import RTAudRotten from '@app/assets/rt_aud_rotten.svg';
|
||||||
import RTFresh from '@app/assets/rt_fresh.svg';
|
import RTFresh from '@app/assets/rt_fresh.svg';
|
||||||
import RTRotten from '@app/assets/rt_rotten.svg';
|
import RTRotten from '@app/assets/rt_rotten.svg';
|
||||||
|
import ImdbLogo from '@app/assets/services/imdb.svg';
|
||||||
import TmdbLogo from '@app/assets/tmdb_logo.svg';
|
import TmdbLogo from '@app/assets/tmdb_logo.svg';
|
||||||
import Button from '@app/components/Common/Button';
|
import Button from '@app/components/Common/Button';
|
||||||
import CachedImage from '@app/components/Common/CachedImage';
|
import CachedImage from '@app/components/Common/CachedImage';
|
||||||
@@ -40,7 +41,7 @@ import {
|
|||||||
ChevronDoubleDownIcon,
|
ChevronDoubleDownIcon,
|
||||||
ChevronDoubleUpIcon,
|
ChevronDoubleUpIcon,
|
||||||
} from '@heroicons/react/24/solid';
|
} from '@heroicons/react/24/solid';
|
||||||
import type { RTRating } from '@server/api/rottentomatoes';
|
import { type RatingResponse } from '@server/api/ratings';
|
||||||
import { IssueStatus } from '@server/constants/issue';
|
import { IssueStatus } from '@server/constants/issue';
|
||||||
import { MediaStatus } from '@server/constants/media';
|
import { MediaStatus } from '@server/constants/media';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
@@ -91,6 +92,7 @@ const messages = defineMessages({
|
|||||||
rtcriticsscore: 'Rotten Tomatoes Tomatometer',
|
rtcriticsscore: 'Rotten Tomatoes Tomatometer',
|
||||||
rtaudiencescore: 'Rotten Tomatoes Audience Score',
|
rtaudiencescore: 'Rotten Tomatoes Audience Score',
|
||||||
tmdbuserscore: 'TMDB User Score',
|
tmdbuserscore: 'TMDB User Score',
|
||||||
|
imdbuserscore: 'IMDB User Score',
|
||||||
});
|
});
|
||||||
|
|
||||||
interface MovieDetailsProps {
|
interface MovieDetailsProps {
|
||||||
@@ -126,8 +128,8 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: ratingData } = useSWR<RTRating>(
|
const { data: ratingData } = useSWR<RatingResponse>(
|
||||||
`/api/v1/movie/${router.query.movieId}/ratings`
|
`/api/v1/movie/${router.query.movieId}/ratingscombined`
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortedCrew = useMemo(
|
const sortedCrew = useMemo(
|
||||||
@@ -541,44 +543,62 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
)}
|
)}
|
||||||
<div className="media-facts">
|
<div className="media-facts">
|
||||||
{(!!data.voteCount ||
|
{(!!data.voteCount ||
|
||||||
(ratingData?.criticsRating && !!ratingData?.criticsScore) ||
|
(ratingData?.rt?.criticsRating &&
|
||||||
(ratingData?.audienceRating && !!ratingData?.audienceScore)) && (
|
!!ratingData?.rt?.criticsScore) ||
|
||||||
|
(ratingData?.rt?.audienceRating &&
|
||||||
|
!!ratingData?.rt?.audienceScore) ||
|
||||||
|
ratingData?.imdb?.criticsScore) && (
|
||||||
<div className="media-ratings">
|
<div className="media-ratings">
|
||||||
{ratingData?.criticsRating && !!ratingData?.criticsScore && (
|
{ratingData?.rt?.criticsRating &&
|
||||||
|
!!ratingData?.rt?.criticsScore && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={intl.formatMessage(messages.rtcriticsscore)}
|
content={intl.formatMessage(messages.rtcriticsscore)}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={ratingData.url}
|
href={ratingData.rt.url}
|
||||||
className="media-rating"
|
className="media-rating"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{ratingData.criticsRating === 'Rotten' ? (
|
{ratingData.rt.criticsRating === 'Rotten' ? (
|
||||||
<RTRotten className="w-6" />
|
<RTRotten className="w-6" />
|
||||||
) : (
|
) : (
|
||||||
<RTFresh className="w-6" />
|
<RTFresh className="w-6" />
|
||||||
)}
|
)}
|
||||||
<span>{ratingData.criticsScore}%</span>
|
<span>{ratingData.rt.criticsScore}%</span>
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
{ratingData?.audienceRating && !!ratingData?.audienceScore && (
|
{ratingData?.rt?.audienceRating &&
|
||||||
|
!!ratingData?.rt?.audienceScore && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={intl.formatMessage(messages.rtaudiencescore)}
|
content={intl.formatMessage(messages.rtaudiencescore)}
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href={ratingData.url}
|
href={ratingData.rt.url}
|
||||||
className="media-rating"
|
className="media-rating"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
{ratingData.audienceRating === 'Spilled' ? (
|
{ratingData.rt.audienceRating === 'Spilled' ? (
|
||||||
<RTAudRotten className="w-6" />
|
<RTAudRotten className="w-6" />
|
||||||
) : (
|
) : (
|
||||||
<RTAudFresh className="w-6" />
|
<RTAudFresh className="w-6" />
|
||||||
)}
|
)}
|
||||||
<span>{ratingData.audienceScore}%</span>
|
<span>{ratingData.rt.audienceScore}%</span>
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{ratingData?.imdb?.criticsScore && (
|
||||||
|
<Tooltip content={intl.formatMessage(messages.imdbuserscore)}>
|
||||||
|
<a
|
||||||
|
href={ratingData.imdb.url}
|
||||||
|
className="media-rating"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<ImdbLogo className="mr-1 w-6" />
|
||||||
|
<span>{ratingData.imdb.criticsScore}</span>
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -827,7 +847,7 @@ const MovieDetails = ({ movie }: MovieDetailsProps) => {
|
|||||||
tmdbId={data.id}
|
tmdbId={data.id}
|
||||||
tvdbId={data.externalIds.tvdbId}
|
tvdbId={data.externalIds.tvdbId}
|
||||||
imdbId={data.externalIds.imdbId}
|
imdbId={data.externalIds.imdbId}
|
||||||
rtUrl={ratingData?.url}
|
rtUrl={ratingData?.rt?.url}
|
||||||
mediaUrl={
|
mediaUrl={
|
||||||
data.mediaInfo?.mediaUrl ?? data.mediaInfo?.mediaUrl4k
|
data.mediaInfo?.mediaUrl ?? data.mediaInfo?.mediaUrl4k
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -437,6 +437,7 @@ export const WatchProviderSelector = ({
|
|||||||
{otherProviders.length > 0 && (
|
{otherProviders.length > 0 && (
|
||||||
<button
|
<button
|
||||||
className="relative top-4 flex items-center justify-center space-x-2 text-sm text-gray-400 transition hover:text-gray-200"
|
className="relative top-4 flex items-center justify-center space-x-2 text-sm text-gray-400 transition hover:text-gray-200"
|
||||||
|
type="button"
|
||||||
onClick={() => setShowMore(!showMore)}
|
onClick={() => setShowMore(!showMore)}
|
||||||
>
|
>
|
||||||
<div className="h-0.5 flex-1 bg-gray-600" />
|
<div className="h-0.5 flex-1 bg-gray-600" />
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ const defaultPayload = {
|
|||||||
requestedBy_email: '{{requestedBy_email}}',
|
requestedBy_email: '{{requestedBy_email}}',
|
||||||
requestedBy_username: '{{requestedBy_username}}',
|
requestedBy_username: '{{requestedBy_username}}',
|
||||||
requestedBy_avatar: '{{requestedBy_avatar}}',
|
requestedBy_avatar: '{{requestedBy_avatar}}',
|
||||||
|
requestedBy_settings_discordId: '{{requestedBy_settings_discordId}}',
|
||||||
|
requestedBy_settings_telegramChatId:
|
||||||
|
'{{requestedBy_settings_telegramChatId}}',
|
||||||
},
|
},
|
||||||
'{{issue}}': {
|
'{{issue}}': {
|
||||||
issue_id: '{{issue_id}}',
|
issue_id: '{{issue_id}}',
|
||||||
@@ -47,12 +50,18 @@ const defaultPayload = {
|
|||||||
reportedBy_email: '{{reportedBy_email}}',
|
reportedBy_email: '{{reportedBy_email}}',
|
||||||
reportedBy_username: '{{reportedBy_username}}',
|
reportedBy_username: '{{reportedBy_username}}',
|
||||||
reportedBy_avatar: '{{reportedBy_avatar}}',
|
reportedBy_avatar: '{{reportedBy_avatar}}',
|
||||||
|
reportedBy_settings_discordId: '{{reportedBy_settings_discordId}}',
|
||||||
|
reportedBy_settings_telegramChatId:
|
||||||
|
'{{reportedBy_settings_telegramChatId}}',
|
||||||
},
|
},
|
||||||
'{{comment}}': {
|
'{{comment}}': {
|
||||||
comment_message: '{{comment_message}}',
|
comment_message: '{{comment_message}}',
|
||||||
commentedBy_email: '{{commentedBy_email}}',
|
commentedBy_email: '{{commentedBy_email}}',
|
||||||
commentedBy_username: '{{commentedBy_username}}',
|
commentedBy_username: '{{commentedBy_username}}',
|
||||||
commentedBy_avatar: '{{commentedBy_avatar}}',
|
commentedBy_avatar: '{{commentedBy_avatar}}',
|
||||||
|
commentedBy_settings_discordId: '{{commentedBy_settings_discordId}}',
|
||||||
|
commentedBy_settings_telegramChatId:
|
||||||
|
'{{commentedBy_settings_telegramChatId}}',
|
||||||
},
|
},
|
||||||
'{{extra}}': [],
|
'{{extra}}': [],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ const Slider = ({
|
|||||||
}`}
|
}`}
|
||||||
onClick={() => slide(Direction.LEFT)}
|
onClick={() => slide(Direction.LEFT)}
|
||||||
disabled={scrollPos.isStart}
|
disabled={scrollPos.isStart}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon className="h-6 w-6" />
|
<ChevronLeftIcon className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
@@ -165,6 +166,7 @@ const Slider = ({
|
|||||||
}`}
|
}`}
|
||||||
onClick={() => slide(Direction.RIGHT)}
|
onClick={() => slide(Direction.RIGHT)}
|
||||||
disabled={scrollPos.isEnd}
|
disabled={scrollPos.isEnd}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<ChevronRightIcon className="h-6 w-6" />
|
<ChevronRightIcon className="h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -176,11 +176,11 @@ const StatusBadge = ({
|
|||||||
</span>
|
</span>
|
||||||
{inProgress && (
|
{inProgress && (
|
||||||
<>
|
<>
|
||||||
{mediaType === 'tv' && (
|
{mediaType === 'tv' && downloadItem[0].episode && (
|
||||||
<span className="ml-1">
|
<span className="ml-1">
|
||||||
{intl.formatMessage(messages.seasonepisodenumber, {
|
{intl.formatMessage(messages.seasonepisodenumber, {
|
||||||
seasonNumber: downloadItem[0].episode?.seasonNumber,
|
seasonNumber: downloadItem[0].episode.seasonNumber,
|
||||||
episodeNumber: downloadItem[0].episode?.episodeNumber,
|
episodeNumber: downloadItem[0].episode.episodeNumber,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -229,11 +229,11 @@ const StatusBadge = ({
|
|||||||
</span>
|
</span>
|
||||||
{inProgress && (
|
{inProgress && (
|
||||||
<>
|
<>
|
||||||
{mediaType === 'tv' && (
|
{mediaType === 'tv' && downloadItem[0].episode && (
|
||||||
<span className="ml-1">
|
<span className="ml-1">
|
||||||
{intl.formatMessage(messages.seasonepisodenumber, {
|
{intl.formatMessage(messages.seasonepisodenumber, {
|
||||||
seasonNumber: downloadItem[0].episode?.seasonNumber,
|
seasonNumber: downloadItem[0].episode.seasonNumber,
|
||||||
episodeNumber: downloadItem[0].episode?.episodeNumber,
|
episodeNumber: downloadItem[0].episode.episodeNumber,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -282,11 +282,11 @@ const StatusBadge = ({
|
|||||||
</span>
|
</span>
|
||||||
{inProgress && (
|
{inProgress && (
|
||||||
<>
|
<>
|
||||||
{mediaType === 'tv' && (
|
{mediaType === 'tv' && downloadItem[0].episode && (
|
||||||
<span className="ml-1">
|
<span className="ml-1">
|
||||||
{intl.formatMessage(messages.seasonepisodenumber, {
|
{intl.formatMessage(messages.seasonepisodenumber, {
|
||||||
seasonNumber: downloadItem[0].episode?.seasonNumber,
|
seasonNumber: downloadItem[0].episode.seasonNumber,
|
||||||
episodeNumber: downloadItem[0].episode?.episodeNumber,
|
episodeNumber: downloadItem[0].episode.episodeNumber,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import {
|
|||||||
PlayIcon,
|
PlayIcon,
|
||||||
} from '@heroicons/react/24/outline';
|
} from '@heroicons/react/24/outline';
|
||||||
import { ChevronDownIcon } from '@heroicons/react/24/solid';
|
import { ChevronDownIcon } from '@heroicons/react/24/solid';
|
||||||
import type { RTRating } from '@server/api/rottentomatoes';
|
import type { RTRating } from '@server/api/rating/rottentomatoes';
|
||||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||||
import { IssueStatus } from '@server/constants/issue';
|
import { IssueStatus } from '@server/constants/issue';
|
||||||
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
|
||||||
|
|||||||
@@ -200,8 +200,6 @@
|
|||||||
"components.LanguageSelector.originalLanguageDefault": "All Languages",
|
"components.LanguageSelector.originalLanguageDefault": "All Languages",
|
||||||
"components.Layout.LanguagePicker.displaylanguage": "Display Language",
|
"components.Layout.LanguagePicker.displaylanguage": "Display Language",
|
||||||
"components.Layout.SearchInput.searchPlaceholder": "Search Movies & TV",
|
"components.Layout.SearchInput.searchPlaceholder": "Search Movies & TV",
|
||||||
"components.Layout.Sidebar.browsemovies": "Movies",
|
|
||||||
"components.Layout.Sidebar.browsetv": "Series",
|
|
||||||
"components.Layout.Sidebar.dashboard": "Discover",
|
"components.Layout.Sidebar.dashboard": "Discover",
|
||||||
"components.Layout.Sidebar.browsemovies": "Movies",
|
"components.Layout.Sidebar.browsemovies": "Movies",
|
||||||
"components.Layout.Sidebar.browsetv": "Series",
|
"components.Layout.Sidebar.browsetv": "Series",
|
||||||
@@ -282,6 +280,7 @@
|
|||||||
"components.MovieDetails.cast": "Cast",
|
"components.MovieDetails.cast": "Cast",
|
||||||
"components.MovieDetails.digitalrelease": "Digital Release",
|
"components.MovieDetails.digitalrelease": "Digital Release",
|
||||||
"components.MovieDetails.downloadstatus": "Download Status",
|
"components.MovieDetails.downloadstatus": "Download Status",
|
||||||
|
"components.MovieDetails.imdbuserscore": "IMDB User Score",
|
||||||
"components.MovieDetails.managemovie": "Manage Movie",
|
"components.MovieDetails.managemovie": "Manage Movie",
|
||||||
"components.MovieDetails.mark4kavailable": "Mark as Available in 4K",
|
"components.MovieDetails.mark4kavailable": "Mark as Available in 4K",
|
||||||
"components.MovieDetails.markavailable": "Mark as Available",
|
"components.MovieDetails.markavailable": "Mark as Available",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user