mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
Compare commits
6 Commits
renovate/s
...
23d05a5921
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23d05a5921 | ||
|
|
3ee69663dc | ||
|
|
539d49879d | ||
|
|
15356dfe49 | ||
|
|
1f04eeb040 | ||
|
|
e3028c21f2 |
@@ -8,7 +8,7 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://discord.gg/seerr"><img src="https://img.shields.io/discord/783137440809746482" alt="Discord"></a>
|
<a href="https://discord.gg/seerr"><img src="https://img.shields.io/discord/783137440809746482" alt="Discord"></a>
|
||||||
<a href="https://hub.docker.com/r/seerr/seerr"><img src="https://img.shields.io/docker/pulls/seerr/seerr" alt="Docker pulls"></a>
|
<a href="https://hub.docker.com/r/seerr/seerr"><img src="https://img.shields.io/docker/pulls/seerr/seerr" alt="Docker pulls"></a>
|
||||||
<a href="https://translate.seerr.dev/engage/seerr/"><img src="https://translate.seerr.dev/widget/seerr/seerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
<a href="https://translate.seerr.dev/engage/seerr/"><img src="https://translate.seerr.dev/widget/seerr/svg-badge.svg" alt="Translation status" /></a>
|
||||||
<a href="https://github.com/seerr-team/seerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/seerr-team/seerr"></a>
|
<a href="https://github.com/seerr-team/seerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/seerr-team/seerr"></a>
|
||||||
|
|
||||||
**Seerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
|
**Seerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
|
||||||
|
|||||||
@@ -174,4 +174,36 @@ This can happen if you have a new installation of Jellyfin/Emby or if you have c
|
|||||||
|
|
||||||
This process should restore your admin privileges while preserving your settings.
|
This process should restore your admin privileges while preserving your settings.
|
||||||
|
|
||||||
|
## Failed to enable web push notifications
|
||||||
|
|
||||||
|
### Option 1: You are using Pi-hole
|
||||||
|
|
||||||
|
When using Pi-hole, you need to whitelist the proper domains in order for the queries to not be intercepted and blocked by Pi-hole.
|
||||||
|
If you are using a chromium based browser (eg: Chrome, Brave, Edge...), the domain you need to whitelist is `fcm.googleapis.com`
|
||||||
|
If you are using Firefox, the domain you need to whitelist is `push.services.mozilla.com`
|
||||||
|
|
||||||
|
1. Log into your Pi-hole through the admin interface, then click on Domains situated under GROUP MANAGEMENT.
|
||||||
|
2. Add the domain corresponding to your browser in the `Domain to be added` field and then click on Add to allowed domains.
|
||||||
|
3. Now in order for those changes to be used you need to flush your current dns cache.
|
||||||
|
4. You can do so by using this command line in your Pi-hole terminal:
|
||||||
|
```bash
|
||||||
|
pihole restartdns
|
||||||
|
```
|
||||||
|
If this command fails (which is unlikely), use this equivalent:
|
||||||
|
```bash
|
||||||
|
pihole -f && pihole restartdns
|
||||||
|
```
|
||||||
|
5. Then restart your Seerr instance and try to enable the web push notifications again.
|
||||||
|
|
||||||
|
|
||||||
|
### Option 2: You are using Brave browser
|
||||||
|
|
||||||
|
Brave is a "De-Googled" browser. So by default or if you refused a prompt in the past, it cuts the access to the FCM (Firebase Cloud Messaging) service, which is mandatory for the web push notifications on Chromium based browsers.
|
||||||
|
|
||||||
|
1. Open Brave and paste this address in the url bar: `brave://settings/privacy`
|
||||||
|
2. Look for the option: "Use Google services for push messaging"
|
||||||
|
3. Activate this option
|
||||||
|
4. Relaunch Brave completely
|
||||||
|
5. You should now see the notifications prompt appearing instead of an error message.
|
||||||
|
|
||||||
If you still encounter issues, please reach out on our support channels.
|
If you still encounter issues, please reach out on our support channels.
|
||||||
|
|||||||
4679
pnpm-lock.yaml
generated
4679
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -112,6 +112,10 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
|
|||||||
DateCreated?: string;
|
DateCreated?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EpisodeReturn<T> = T extends { includeMediaInfo: true }
|
||||||
|
? JellyfinLibraryItemExtended[]
|
||||||
|
: JellyfinLibraryItem[];
|
||||||
|
|
||||||
export interface JellyfinItemsReponse {
|
export interface JellyfinItemsReponse {
|
||||||
Items: JellyfinLibraryItemExtended[];
|
Items: JellyfinLibraryItemExtended[];
|
||||||
TotalRecordCount: number;
|
TotalRecordCount: number;
|
||||||
@@ -415,13 +419,22 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getEpisodes(
|
public async getEpisodes<
|
||||||
|
T extends { includeMediaInfo?: boolean } | undefined = undefined
|
||||||
|
>(
|
||||||
seriesID: string,
|
seriesID: string,
|
||||||
seasonID: string
|
seasonID: string,
|
||||||
): Promise<JellyfinLibraryItem[]> {
|
options?: T
|
||||||
|
): Promise<EpisodeReturn<T>> {
|
||||||
try {
|
try {
|
||||||
const episodeResponse = await this.get<any>(
|
const episodeResponse = await this.get<any>(
|
||||||
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
|
`/Shows/${seriesID}/Episodes`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
seasonId: seasonID,
|
||||||
|
...(options?.includeMediaInfo && { fields: 'MediaSources' }),
|
||||||
|
},
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return episodeResponse.Items.filter(
|
return episodeResponse.Items.filter(
|
||||||
|
|||||||
@@ -374,9 +374,10 @@ class JellyfinScanner {
|
|||||||
) ?? []
|
) ?? []
|
||||||
).length;
|
).length;
|
||||||
|
|
||||||
|
const jellyfinSeasons = await this.jfClient.getSeasons(Id);
|
||||||
|
|
||||||
for (const season of seasons) {
|
for (const season of seasons) {
|
||||||
const JellyfinSeasons = await this.jfClient.getSeasons(Id);
|
const matchedJellyfinSeason = jellyfinSeasons.find((md) => {
|
||||||
const matchedJellyfinSeason = JellyfinSeasons.find((md) => {
|
|
||||||
if (tvdbSeasonFromAnidb) {
|
if (tvdbSeasonFromAnidb) {
|
||||||
// In AniDB we don't have the concept of seasons,
|
// In AniDB we don't have the concept of seasons,
|
||||||
// we have multiple shows with only Season 1 (and sometimes a season with index 0 for specials).
|
// we have multiple shows with only Season 1 (and sometimes a season with index 0 for specials).
|
||||||
@@ -397,38 +398,52 @@ class JellyfinScanner {
|
|||||||
|
|
||||||
// Check if we found the matching season and it has all the available episodes
|
// Check if we found the matching season and it has all the available episodes
|
||||||
if (matchedJellyfinSeason) {
|
if (matchedJellyfinSeason) {
|
||||||
// If we have a matched Jellyfin season, get its children metadata so we can check details
|
|
||||||
const episodes = await this.jfClient.getEpisodes(
|
|
||||||
Id,
|
|
||||||
matchedJellyfinSeason.Id
|
|
||||||
);
|
|
||||||
|
|
||||||
//Get count of episodes that are HD and 4K
|
|
||||||
let totalStandard = 0;
|
let totalStandard = 0;
|
||||||
let total4k = 0;
|
let total4k = 0;
|
||||||
|
|
||||||
//use for loop to make sure this loop _completes_ in full
|
if (!this.enable4kShow) {
|
||||||
//before the next section
|
const episodes = await this.jfClient.getEpisodes(
|
||||||
for (const episode of episodes) {
|
Id,
|
||||||
let episodeCount = 1;
|
matchedJellyfinSeason.Id
|
||||||
|
);
|
||||||
|
|
||||||
// count number of combined episodes
|
for (const episode of episodes) {
|
||||||
if (
|
let episodeCount = 1;
|
||||||
episode.IndexNumber !== undefined &&
|
|
||||||
episode.IndexNumberEnd !== undefined
|
// count number of combined episodes
|
||||||
) {
|
if (
|
||||||
episodeCount =
|
episode.IndexNumber !== undefined &&
|
||||||
episode.IndexNumberEnd - episode.IndexNumber + 1;
|
episode.IndexNumberEnd !== undefined
|
||||||
}
|
) {
|
||||||
|
episodeCount =
|
||||||
|
episode.IndexNumberEnd - episode.IndexNumber + 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.enable4kShow) {
|
|
||||||
totalStandard += episodeCount;
|
totalStandard += episodeCount;
|
||||||
} else {
|
}
|
||||||
const ExtendedEpisodeData = await this.jfClient.getItemData(
|
} else {
|
||||||
episode.Id
|
// 4K detection enabled - request media info to check resolution
|
||||||
);
|
const episodes = await this.jfClient.getEpisodes(
|
||||||
|
Id,
|
||||||
|
matchedJellyfinSeason.Id,
|
||||||
|
{ includeMediaInfo: true }
|
||||||
|
);
|
||||||
|
|
||||||
ExtendedEpisodeData?.MediaSources?.some((MediaSource) => {
|
for (const episode of episodes) {
|
||||||
|
let episodeCount = 1;
|
||||||
|
|
||||||
|
// count number of combined episodes
|
||||||
|
if (
|
||||||
|
episode.IndexNumber !== undefined &&
|
||||||
|
episode.IndexNumberEnd !== undefined
|
||||||
|
) {
|
||||||
|
episodeCount =
|
||||||
|
episode.IndexNumberEnd - episode.IndexNumber + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MediaSources field is included in response when includeMediaInfo is true
|
||||||
|
// We iterate all MediaSources to detect if episode has both standard AND 4K versions
|
||||||
|
episode.MediaSources?.some((MediaSource) => {
|
||||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||||
if (MediaStream.Type === 'Video') {
|
if (MediaStream.Type === 'Video') {
|
||||||
if ((MediaStream.Width ?? 0) >= 2000) {
|
if ((MediaStream.Width ?? 0) >= 2000) {
|
||||||
|
|||||||
@@ -626,76 +626,6 @@ authRoutes.post('/local', async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainUser = await userRepository.findOneOrFail({
|
|
||||||
select: { id: true, plexToken: true, plexId: true },
|
|
||||||
where: { id: 1 },
|
|
||||||
});
|
|
||||||
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
|
|
||||||
|
|
||||||
if (!user.plexId) {
|
|
||||||
try {
|
|
||||||
const plexUsersResponse = await mainPlexTv.getUsers();
|
|
||||||
const account = plexUsersResponse.MediaContainer.User.find(
|
|
||||||
(account) =>
|
|
||||||
account.$.email &&
|
|
||||||
account.$.email.toLowerCase() === user.email.toLowerCase()
|
|
||||||
)?.$;
|
|
||||||
|
|
||||||
if (
|
|
||||||
account &&
|
|
||||||
(await mainPlexTv.checkUserAccess(parseInt(account.id)))
|
|
||||||
) {
|
|
||||||
logger.info(
|
|
||||||
'Found matching Plex user; updating user with Plex data',
|
|
||||||
{
|
|
||||||
label: 'API',
|
|
||||||
ip: req.ip,
|
|
||||||
email: body.email,
|
|
||||||
userId: user.id,
|
|
||||||
plexId: account.id,
|
|
||||||
plexUsername: account.username,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
user.plexId = parseInt(account.id);
|
|
||||||
user.avatar = account.thumb;
|
|
||||||
user.email = account.email;
|
|
||||||
user.plexUsername = account.username;
|
|
||||||
user.userType = UserType.PLEX;
|
|
||||||
|
|
||||||
await userRepository.save(user);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Something went wrong fetching Plex users', {
|
|
||||||
label: 'API',
|
|
||||||
errorMessage: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
user.plexId &&
|
|
||||||
user.plexId !== mainUser.plexId &&
|
|
||||||
!(await mainPlexTv.checkUserAccess(user.plexId))
|
|
||||||
) {
|
|
||||||
logger.warn(
|
|
||||||
'Failed sign-in attempt from Plex user without access to the media server',
|
|
||||||
{
|
|
||||||
label: 'API',
|
|
||||||
account: {
|
|
||||||
ip: req.ip,
|
|
||||||
email: body.email,
|
|
||||||
userId: user.id,
|
|
||||||
plexId: user.plexId,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return next({
|
|
||||||
status: 403,
|
|
||||||
message: 'Access denied.',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set logged in session
|
// Set logged in session
|
||||||
if (user && req.session) {
|
if (user && req.session) {
|
||||||
req.session.userId = user.id;
|
req.session.userId = user.id;
|
||||||
@@ -775,7 +705,7 @@ authRoutes.post('/logout', async (req, res, next) => {
|
|||||||
});
|
});
|
||||||
return next({ status: 500, message: 'Failed to destroy session.' });
|
return next({ status: 500, message: 'Failed to destroy session.' });
|
||||||
}
|
}
|
||||||
logger.info('Successfully logged out user', {
|
logger.debug('Successfully logged out user', {
|
||||||
label: 'Auth',
|
label: 'Auth',
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { UserType } from '@server/constants/user';
|
|||||||
import type { PermissionCheckOptions } from '@server/lib/permissions';
|
import type { PermissionCheckOptions } from '@server/lib/permissions';
|
||||||
import { hasPermission, Permission } from '@server/lib/permissions';
|
import { hasPermission, Permission } from '@server/lib/permissions';
|
||||||
import type { NotificationAgentKey } from '@server/lib/settings';
|
import type { NotificationAgentKey } from '@server/lib/settings';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import type { MutatorCallback } from 'swr';
|
import type { MutatorCallback } from 'swr';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
@@ -56,13 +57,21 @@ export const useUser = ({
|
|||||||
id,
|
id,
|
||||||
initialData,
|
initialData,
|
||||||
}: { id?: number; initialData?: User } = {}): UserHookResponse => {
|
}: { id?: number; initialData?: User } = {}): UserHookResponse => {
|
||||||
|
const router = useRouter();
|
||||||
|
const isAuthPage = /^\/(login|setup|resetpassword(?:\/|$))/.test(
|
||||||
|
router.pathname
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
error,
|
error,
|
||||||
mutate: revalidate,
|
mutate: revalidate,
|
||||||
} = useSWR<User>(id ? `/api/v1/user/${id}` : `/api/v1/auth/me`, {
|
} = useSWR<User>(id ? `/api/v1/user/${id}` : `/api/v1/auth/me`, {
|
||||||
fallbackData: initialData,
|
fallbackData: initialData,
|
||||||
refreshInterval: 30000,
|
refreshInterval: !isAuthPage ? 30000 : 0,
|
||||||
|
revalidateOnFocus: !isAuthPage,
|
||||||
|
revalidateOnMount: !isAuthPage,
|
||||||
|
revalidateOnReconnect: !isAuthPage,
|
||||||
errorRetryInterval: 30000,
|
errorRetryInterval: 30000,
|
||||||
shouldRetryOnError: false,
|
shouldRetryOnError: false,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user