Compare commits

..

1 Commits

Author SHA1 Message Date
Fallenbagel
66357019f0 fix(backend): fixes Jellyfin/Emby links if server is initially setup with a trailing /
Fixes #168 and #220
2022-09-26 10:17:18 +05:00
9 changed files with 45 additions and 281 deletions

View File

@@ -5470,23 +5470,6 @@ paths:
responses:
'204':
description: Succesfully removed media item
/media/{mediaId}/file:
delete:
summary: Delete media file
description: Removes a media file from radarr/sonarr. The `ADMIN` permission is required to perform this action.
tags:
- media
parameters:
- in: path
name: mediaId
description: Media ID
required: true
example: '1'
schema:
type: string
responses:
'204':
description: Succesfully removed media item
/media/{mediaId}/{status}:
post:
summary: Update media status

View File

@@ -213,20 +213,6 @@ class RadarrAPI extends ServarrBase<{ movieId: number }> {
);
}
}
public removeMovie = async (movieId: number): Promise<void> => {
try {
const { id, title } = await this.getMovieByTmdbId(movieId);
await this.axios.delete(`/movie/${id}`, {
params: {
deleteFiles: true,
addImportExclusion: false,
},
});
logger.info(`[Radarr] Removed movie ${title}`);
} catch (e) {
throw new Error(`[Radarr] Failed to remove movie: ${e.message}`);
}
};
}
export default RadarrAPI;

View File

@@ -302,20 +302,6 @@ class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
return newSeasons;
}
public removeSerie = async (serieId: number): Promise<void> => {
try {
const { id, title } = await this.getSeriesByTvdbId(serieId);
await this.axios.delete(`/series/${id}`, {
params: {
deleteFiles: true,
addImportExclusion: false,
},
});
logger.info(`[Radarr] Removed serie ${title}`);
} catch (e) {
throw new Error(`[Radarr] Failed to remove serie: ${e.message}`);
}
};
}
export default SonarrAPI;

View File

@@ -205,10 +205,16 @@ class Media {
? externalHostname
: hostname;
if (this.jellyfinMediaId) {
this.mediaUrl = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`;
this.mediaUrl = new URL(
`/web/index.html#!/${pageName}?id=${this.jellyfinMediaId}&context=home&serverId=${serverId}`,
jellyfinHost
).href;
}
if (this.jellyfinMediaId4k) {
this.mediaUrl4k = `${jellyfinHost}/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${serverId}`;
this.mediaUrl4k = new URL(
`/web/index.html#!/${pageName}?id=${this.jellyfinMediaId4k}&context=home&serverId=${serverId}`,
jellyfinHost
).href;
}
}
}

View File

@@ -263,7 +263,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
// Update the users avatar with their jellyfin profile pic (incase it changed)
if (account.User.PrimaryImageTag) {
user.avatar = `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`;
user.avatar = new URL(
`/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`,
jellyfinHost
).href;
} else {
user.avatar = '/os_logo_square.png';
}
@@ -309,7 +312,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinAuthToken: account.AccessToken,
permissions: Permission.ADMIN,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
? new URL(
`/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`,
jellyfinHost
).href
: '/os_logo_square.png',
userType: UserType.JELLYFIN,
});
@@ -339,7 +345,10 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
jellyfinAuthToken: account.AccessToken,
permissions: settings.main.defaultPermissions,
avatar: account.User.PrimaryImageTag
? `${jellyfinHost}/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`
? new URL(
`/Users/${account.User.Id}/Images/Primary/?tag=${account.User.PrimaryImageTag}&quality=90`,
jellyfinHost
).href
: '/os_logo_square.png',
userType: UserType.JELLYFIN,
});

View File

@@ -1,7 +1,4 @@
import RadarrAPI from '@server/api/servarr/radarr';
import SonarrAPI from '@server/api/servarr/sonarr';
import TautulliAPI from '@server/api/tautulli';
import TheMovieDb from '@server/api/themoviedb';
import { MediaStatus, MediaType } from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
@@ -171,100 +168,6 @@ mediaRoutes.delete(
}
);
mediaRoutes.delete(
'/:id/file',
isAuthenticated(Permission.MANAGE_REQUESTS),
async (req, res, next) => {
try {
const settings = getSettings();
const mediaRepository = getRepository(Media);
const media = await mediaRepository.findOneOrFail({
where: { id: Number(req.params.id) },
});
const is4k = media.serviceUrl4k !== undefined;
const isMovie = media.mediaType === MediaType.MOVIE;
let serviceSettings;
if (isMovie) {
serviceSettings = settings.radarr.find(
(radarr) => radarr.isDefault && radarr.is4k === is4k
);
} else {
serviceSettings = settings.sonarr.find(
(sonarr) => sonarr.isDefault && sonarr.is4k === is4k
);
}
if (
media.serviceId &&
media.serviceId >= 0 &&
serviceSettings?.id !== media.serviceId
) {
if (isMovie) {
serviceSettings = settings.radarr.find(
(radarr) => radarr.id === media.serviceId
);
} else {
serviceSettings = settings.sonarr.find(
(sonarr) => sonarr.id === media.serviceId
);
}
}
if (!serviceSettings) {
logger.warn(
`There is no default ${
is4k ? '4K ' : '' + isMovie ? 'Radarr' : 'Sonarr'
}/ server configured. Did you set any of your ${
is4k ? '4K ' : '' + isMovie ? 'Radarr' : 'Sonarr'
} servers as default?`,
{
label: 'Media Request',
mediaId: media.id,
}
);
return;
}
let service;
if (isMovie) {
service = new RadarrAPI({
apiKey: serviceSettings?.apiKey,
url: RadarrAPI.buildUrl(serviceSettings, '/api/v3'),
});
} else {
service = new SonarrAPI({
apiKey: serviceSettings?.apiKey,
url: SonarrAPI.buildUrl(serviceSettings, '/api/v3'),
});
}
if (isMovie) {
await (service as RadarrAPI).removeMovie(
parseInt(
is4k
? (media.externalServiceSlug4k as string)
: (media.externalServiceSlug as string)
)
);
} else {
const tmdb = new TheMovieDb();
const series = await tmdb.getTvShow({ tvId: media.tmdbId });
const tvdbId = series.external_ids.tvdb_id ?? media.tvdbId;
if (!tvdbId) {
throw new Error('TVDB ID not found');
}
await (service as SonarrAPI).removeSerie(tvdbId);
}
return res.status(204).send();
} catch (e) {
logger.error('Something went wrong fetching media in delete request', {
label: 'Media',
message: e.message,
});
next({ status: 404, message: 'Media not found' });
}
}
);
mediaRoutes.get<{ id: string }, MediaWatchDataResponse>(
'/:id/watch_data',
isAuthenticated(Permission.ADMIN),

View File

@@ -330,7 +330,10 @@ settingsRoutes.get('/jellyfin/users', async (req, res) => {
username: user.Name,
id: user.Id,
thumb: user.PrimaryImageTag
? `${jellyfinHost}/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`
? new URL(
`/Users/${user.Id}/Images/Primary/?tag=${user.PrimaryImageTag}&quality=90`,
jellyfinHost
).href
: '/os_logo_square.png',
email: user.Name,
}));

View File

@@ -525,7 +525,10 @@ router.post(
email: jellyfinUser?.Name,
permissions: settings.main.defaultPermissions,
avatar: jellyfinUser?.PrimaryImageTag
? `${jellyfinHost}/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`
? new URL(
`/Users/${jellyfinUser.Id}/Images/Primary/?tag=${jellyfinUser.PrimaryImageTag}&quality=90`,
jellyfinHost
).href
: '/os_logo_square.png',
userType: UserType.JELLYFIN,
});

View File

@@ -8,20 +8,11 @@ import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import { ServerIcon, ViewListIcon } from '@heroicons/react/outline';
import {
CheckCircleIcon,
DocumentRemoveIcon,
TrashIcon,
} from '@heroicons/react/solid';
import { CheckCircleIcon, DocumentRemoveIcon } from '@heroicons/react/solid';
import { IssueStatus } from '@server/constants/issue';
import {
MediaRequestStatus,
MediaStatus,
MediaType,
} from '@server/constants/media';
import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
import { MediaServerType } from '@server/constants/server';
import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfaces';
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
import type { MovieDetails } from '@server/models/Movie';
import type { TvDetails } from '@server/models/Tv';
import axios from 'axios';
@@ -40,12 +31,8 @@ const messages = defineMessages({
manageModalClearMedia: 'Clear Data',
manageModalClearMediaWarning:
'* This will irreversibly remove all data for this {mediaType}, including any requests. If this item exists in your Plex library, the media information will be recreated during the next scan.',
manageModalRemoveMediaWarning:
'* This will irreversibly remove this {mediaType} from {arr}, including all files.',
openarr: 'Open in {arr}',
removearr: 'Remove from {arr}',
openarr4k: 'Open in 4K {arr}',
removearr4k: 'Remove from 4K {arr}',
downloadstatus: 'Downloads',
markavailable: 'Mark as Available',
mark4kavailable: 'Mark as Available in 4K',
@@ -99,12 +86,6 @@ const ManageSlideOver = ({
? `/api/v1/media/${data.mediaInfo.id}/watch_data`
: null
);
const { data: radarrData } = useSWR<RadarrSettings[]>(
'/api/v1/settings/radarr'
);
const { data: sonarrData } = useSWR<SonarrSettings[]>(
'/api/v1/settings/sonarr'
);
const deleteMedia = async () => {
if (data.mediaInfo) {
@@ -113,35 +94,6 @@ const ManageSlideOver = ({
}
};
const deleteMediaFile = async () => {
if (data.mediaInfo) {
await axios.delete(`/api/v1/media/${data.mediaInfo.id}/file`);
await axios.delete(`/api/v1/media/${data.mediaInfo.id}`);
revalidate();
}
};
const isDefaultService = () => {
if (data.mediaInfo) {
if (data.mediaInfo.mediaType === MediaType.MOVIE) {
return (
radarrData?.find(
(radarr) =>
radarr.isDefault && radarr.id === data.mediaInfo?.serviceId
) !== undefined
);
} else {
return (
sonarrData?.find(
(sonarr) =>
sonarr.isDefault && sonarr.id === data.mediaInfo?.serviceId
) !== undefined
);
}
}
return false;
};
const markAvailable = async (is4k = false) => {
if (data.mediaInfo) {
await axios.post(`/api/v1/media/${data.mediaInfo?.id}/available`, {
@@ -374,40 +326,6 @@ const ManageSlideOver = ({
</Button>
</a>
)}
{hasPermission(Permission.ADMIN) &&
data?.mediaInfo?.serviceUrl &&
isDefaultService() && (
<div>
<ConfirmButton
onClick={() => deleteMediaFile()}
confirmText={intl.formatMessage(
globalMessages.areyousure
)}
className="w-full"
>
<TrashIcon />
<span>
{intl.formatMessage(messages.removearr, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</ConfirmButton>
<div className="mt-1 text-xs text-gray-400">
{intl.formatMessage(
messages.manageModalRemoveMediaWarning,
{
mediaType: intl.formatMessage(
mediaType === 'movie'
? messages.movie
: messages.tvshow
),
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
}
)}
</div>
</div>
)}
</div>
</div>
)}
@@ -513,54 +431,21 @@ const ManageSlideOver = ({
</div>
)}
{data?.mediaInfo?.serviceUrl4k && (
<>
<a
href={data?.mediaInfo?.serviceUrl4k}
target="_blank"
rel="noreferrer"
className="block"
>
<Button buttonType="ghost" className="w-full">
<ServerIcon />
<span>
{intl.formatMessage(messages.openarr4k, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</Button>
</a>
{isDefaultService() && (
<div>
<ConfirmButton
onClick={() => deleteMediaFile()}
confirmText={intl.formatMessage(
globalMessages.areyousure
)}
className="w-full"
>
<TrashIcon />
<span>
{intl.formatMessage(messages.removearr4k, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</ConfirmButton>
<div className="mt-1 text-xs text-gray-400">
{intl.formatMessage(
messages.manageModalRemoveMediaWarning,
{
mediaType: intl.formatMessage(
mediaType === 'movie'
? messages.movie
: messages.tvshow
),
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
}
)}
</div>
</div>
)}
</>
<a
href={data?.mediaInfo?.serviceUrl4k}
target="_blank"
rel="noreferrer"
className="block"
>
<Button buttonType="ghost" className="w-full">
<ServerIcon />
<span>
{intl.formatMessage(messages.openarr4k, {
arr: mediaType === 'movie' ? 'Radarr' : 'Sonarr',
})}
</span>
</Button>
</a>
)}
</div>
</div>