Compare commits

...

6 Commits

Author SHA1 Message Date
renovate[bot]
23d05a5921 chore(deps): update nextjs monorepo to v14.2.35 2025-12-19 03:10:51 +00:00
fallenbagel
3ee69663dc fix(local-login): remove automatic plex linking and reduce logout log verbosity (#2225)
Removed redundant Plex user discovery logic that applies to all media servers currently. This is now
handled explicitly via linked accounts settings page. Also changed the successful logout log level
from info to debug since its routine behaviour
2025-12-15 19:44:43 +08:00
Ludovic Ortega
539d49879d chore: fix translate badge svg url (#2228)
* chore: fix translate badge svg url

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

* fix: use https instead of http

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>

---------

Signed-off-by: Ludovic Ortega <ludovic.ortega@adminafk.fr>
2025-12-14 05:37:36 +08:00
RolliePollie18
15356dfe49 fix(jellyfin-scan): reduce jellyfin API calls during recently added scan (#2205)
* fix(jellyfin scanner): reduce jellyfin API calls during recently added scan

Significantly reduce number of API calls, addressing CPU spikes on Jellyfin 10.10+ servers.- Move
getSeasons() call outside the seasons loop (N calls to 1)- Request MediaSources via getEpisodes()
field parameter instead of  individual getItemData() calls per episode (N calls to 1 per season)
Performance improvements (tested on library with 12 TV shows):- Scan duration: 43.7s to 9.1s - Peak
CPU: 277% to 115% - CPU spike duration: 36s to 2s Functionality is unchanged, all availability
statuses identicalbefore and after.

* fix: add getEpisodes overloads to remove unsafe type assertion

* refactor(jellyfin): use generics instead of overloads

---------

Co-authored-by: patrick-acland <patrick.acland@kraken.tech>
2025-12-09 22:20:47 +08:00
fallenbagel
1f04eeb040 fix: disable automatic auth revalidation on auth pages (#2213)
* fix: disable automatic auth revalidation on auth pages

Prevents unnecessary `/api/v1/auth/me` requests on login, setup, and password reset pages.

fix #738

* fix: update regex to include resetpassword guid & add missing condition in refreshInterval
2025-12-09 13:17:17 +01:00
Thibaut Noah
e3028c21f2 docs: add webpush related troubleshooting steps (#2170)
* Update troubleshooting.mdx

Add potential fixes for users who fail to enable their web push notifications

* Update docs/troubleshooting.mdx

Modify appName syntax for better coding norm

Co-authored-by: Gauthier <mail@gauthierth.fr>

* refactor: apply suggestions from review comments

Co-authored-by: Gauthier <mail@gauthierth.fr>

* docs(troubleshooting): fix typos in troubleshooting doc page

---------

Co-authored-by: Gauthier <mail@gauthierth.fr>
Co-authored-by: fallenbagel <98979876+fallenbagel@users.noreply.github.com>
2025-12-09 08:49:42 +00:00
7 changed files with 1690 additions and 3196 deletions

View File

@@ -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/)**.

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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(

View File

@@ -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) {

View File

@@ -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,
}); });

View File

@@ -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,
}); });