Skip to content

Commit

Permalink
feat(media): add link to the item on plex (#735)
Browse files Browse the repository at this point in the history
Co-authored-by: sct <sctsnipe@gmail.com>
  • Loading branch information
ankarhem and sct committed Jan 28, 2021
1 parent 946bd2d commit 1d7150c
Show file tree
Hide file tree
Showing 13 changed files with 476 additions and 81 deletions.
20 changes: 20 additions & 0 deletions server/entity/Media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,35 @@ class Media {
@Column({ nullable: true })
public externalServiceSlug4k?: string;

@Column({ nullable: true })
public ratingKey?: string;

@Column({ nullable: true })
public ratingKey4k?: string;

public serviceUrl?: string;
public serviceUrl4k?: string;
public downloadStatus?: DownloadingItem[] = [];
public downloadStatus4k?: DownloadingItem[] = [];

public plexUrl?: string;
public plexUrl4k?: string;

constructor(init?: Partial<Media>) {
Object.assign(this, init);
}

@AfterLoad()
public setPlexUrls(): void {
const machineId = getSettings().plex.machineId;
if (this.ratingKey) {
this.plexUrl = `https://app.plex.tv/desktop#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey}`;
}
if (this.ratingKey4k) {
this.plexUrl4k = `https://app.plex.tv/desktop#!/server/${machineId}/details?key=%2Flibrary%2Fmetadata%2F${this.ratingKey4k}`;
}
}

@AfterLoad()
public setServiceUrl(): void {
if (this.mediaType === MediaType.MOVIE) {
Expand Down
76 changes: 71 additions & 5 deletions server/job/plexsync/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class JobPlexSync {

private async processMovie(plexitem: PlexLibraryItem) {
const mediaRepository = getRepository(Media);

try {
if (plexitem.guid.match(plexRegex)) {
const metadata = await this.plexClient.getMetadata(plexitem.ratingKey);
Expand Down Expand Up @@ -138,6 +139,23 @@ class JobPlexSync {
changedExisting = true;
}

if (
(hasOtherResolution || (has4k && !this.enable4kMovie)) &&
existing.ratingKey !== plexitem.ratingKey
) {
existing.ratingKey = plexitem.ratingKey;
changedExisting = true;
}

if (
has4k &&
this.enable4kMovie &&
existing.ratingKey4k !== plexitem.ratingKey
) {
existing.ratingKey4k = plexitem.ratingKey;
changedExisting = true;
}

if (changedExisting) {
await mediaRepository.save(existing);
this.log(
Expand All @@ -160,6 +178,12 @@ class JobPlexSync {
: MediaStatus.UNKNOWN;
newMedia.mediaType = MediaType.MOVIE;
newMedia.mediaAddedAt = new Date(plexitem.addedAt * 1000);
newMedia.ratingKey =
hasOtherResolution || (!this.enable4kMovie && has4k)
? plexitem.ratingKey
: undefined;
newMedia.ratingKey4k =
has4k && this.enable4kMovie ? plexitem.ratingKey : undefined;
await mediaRepository.save(newMedia);
this.log(`Saved ${plexitem.title}`);
}
Expand Down Expand Up @@ -242,6 +266,23 @@ class JobPlexSync {
changedExisting = true;
}

if (
(hasOtherResolution || (has4k && !this.enable4kMovie)) &&
existing.ratingKey !== plexitem.ratingKey
) {
existing.ratingKey = plexitem.ratingKey;
changedExisting = true;
}

if (
has4k &&
this.enable4kMovie &&
existing.ratingKey4k !== plexitem.ratingKey
) {
existing.ratingKey4k = plexitem.ratingKey;
changedExisting = true;
}

if (changedExisting) {
await mediaRepository.save(existing);
this.log(
Expand Down Expand Up @@ -272,6 +313,12 @@ class JobPlexSync {
? MediaStatus.AVAILABLE
: MediaStatus.UNKNOWN;
newMedia.mediaType = MediaType.MOVIE;
newMedia.ratingKey =
hasOtherResolution || (!this.enable4kMovie && has4k)
? plexitem.ratingKey
: undefined;
newMedia.ratingKey4k =
has4k && this.enable4kMovie ? plexitem.ratingKey : undefined;
await mediaRepository.save(newMedia);
this.log(`Saved ${tmdbMovie.title}`);
}
Expand Down Expand Up @@ -311,12 +358,14 @@ class JobPlexSync {
let tvShow: TmdbTvDetails | null = null;

try {
const metadata = await this.plexClient.getMetadata(
const ratingKey =
plexitem.grandparentRatingKey ??
plexitem.parentRatingKey ??
plexitem.ratingKey,
{ includeChildren: true }
);
plexitem.parentRatingKey ??
plexitem.ratingKey;
const metadata = await this.plexClient.getMetadata(ratingKey, {
includeChildren: true,
});

if (metadata.guid.match(tvdbRegex)) {
const matchedtvdb = metadata.guid.match(tvdbRegex);

Expand Down Expand Up @@ -454,6 +503,23 @@ class JobPlexSync {
episode.Media.some((media) => media.videoResolution === '4k')
).length;

if (
media &&
(totalStandard > 0 || (total4k > 0 && !this.enable4kShow)) &&
media.ratingKey !== ratingKey
) {
media.ratingKey = ratingKey;
}

if (
media &&
total4k > 0 &&
this.enable4kShow &&
media.ratingKey4k !== ratingKey
) {
media.ratingKey4k = ratingKey;
}

if (existingSeason) {
// These ternary statements look super confusing, but they are simply
// setting the status to AVAILABLE if all of a type is there, partially if some,
Expand Down
51 changes: 51 additions & 0 deletions server/migration/1611801511397-AddRatingKeysToMedia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddRatingKeysToMedia1611801511397 implements MigrationInterface {
name = 'AddRatingKeysToMedia1611801511397';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(
`CREATE TABLE "temporary_media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "status4k" integer NOT NULL DEFAULT (1), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, "ratingKey" varchar, "ratingKey4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "temporary_media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k" FROM "media"`
);
await queryRunner.query(`DROP TABLE "media"`);
await queryRunner.query(`ALTER TABLE "temporary_media" RENAME TO "media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5"`);
await queryRunner.query(`DROP INDEX "IDX_41a289eb1fa489c1bc6f38d9c3"`);
await queryRunner.query(`DROP INDEX "IDX_7ff2d11f6a83cb52386eaebe74"`);
await queryRunner.query(`ALTER TABLE "media" RENAME TO "temporary_media"`);
await queryRunner.query(
`CREATE TABLE "media" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "mediaType" varchar NOT NULL, "tmdbId" integer NOT NULL, "tvdbId" integer, "imdbId" varchar, "status" integer NOT NULL DEFAULT (1), "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "lastSeasonChange" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "status4k" integer NOT NULL DEFAULT (1), "mediaAddedAt" datetime, "serviceId" integer, "serviceId4k" integer, "externalServiceId" integer, "externalServiceId4k" integer, "externalServiceSlug" varchar, "externalServiceSlug4k" varchar, CONSTRAINT "UQ_41a289eb1fa489c1bc6f38d9c3c" UNIQUE ("tvdbId"))`
);
await queryRunner.query(
`INSERT INTO "media"("id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k") SELECT "id", "mediaType", "tmdbId", "tvdbId", "imdbId", "status", "createdAt", "updatedAt", "lastSeasonChange", "status4k", "mediaAddedAt", "serviceId", "serviceId4k", "externalServiceId", "externalServiceId4k", "externalServiceSlug", "externalServiceSlug4k" FROM "temporary_media"`
);
await queryRunner.query(`DROP TABLE "temporary_media"`);
await queryRunner.query(
`CREATE INDEX "IDX_7157aad07c73f6a6ae3bbd5ef5" ON "media" ("tmdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_41a289eb1fa489c1bc6f38d9c3" ON "media" ("tvdbId") `
);
await queryRunner.query(
`CREATE INDEX "IDX_7ff2d11f6a83cb52386eaebe74" ON "media" ("imdbId") `
);
}
}
1 change: 1 addition & 0 deletions server/models/Movie.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface MovieDetails {
};
mediaInfo?: Media;
externalIds: ExternalIds;
plexUrl?: string;
}

export const mapMovieDetails = (
Expand Down
1 change: 1 addition & 0 deletions src/assets/services/plex.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion src/components/Common/Badge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const Badge: React.FC<BadgeProps> = ({
badgeStyle.push('bg-yellow-500 text-yellow-100');
break;
case 'success':
badgeStyle.push('bg-green-400 text-green-100');
badgeStyle.push('bg-green-500 text-green-100');
break;
default:
badgeStyle.push('bg-indigo-500 text-indigo-100');
Expand Down
74 changes: 59 additions & 15 deletions src/components/Common/ButtonWithDropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,52 +9,94 @@ import useClickOutside from '../../../hooks/useClickOutside';
import Transition from '../../Transition';
import { withProperties } from '../../../utils/typeHelpers';

const DropdownItem: React.FC<AnchorHTMLAttributes<HTMLAnchorElement>> = ({
interface DropdownItemProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
buttonType?: 'primary' | 'ghost';
}

const DropdownItem: React.FC<DropdownItemProps> = ({
children,
buttonType = 'primary',
...props
}) => (
<a
className="flex items-center px-4 py-2 text-sm leading-5 text-white bg-indigo-600 cursor-pointer hover:bg-indigo-500 hover:text-white focus:outline-none focus:border-indigo-700 focus:text-white"
{...props}
>
{children}
</a>
);
}) => {
let styleClass = '';

switch (buttonType) {
case 'ghost':
styleClass =
'text-white bg-gray-700 hover:bg-gray-600 hover:text-white focus:border-gray-500 focus:text-white';
break;
default:
styleClass =
'text-white bg-indigo-600 hover:bg-indigo-500 hover:text-white focus:border-indigo-700 focus:text-white';
}
return (
<a
className={`flex items-center px-4 py-2 text-sm leading-5 cursor-pointer focus:outline-none ${styleClass}`}
{...props}
>
{children}
</a>
);
};

interface ButtonWithDropdownProps
extends ButtonHTMLAttributes<HTMLButtonElement> {
text: ReactNode;
dropdownIcon?: ReactNode;
buttonType?: 'primary' | 'ghost';
}

const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
text,
children,
dropdownIcon,
className,
buttonType = 'primary',
...props
}) => {
const [isOpen, setIsOpen] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
useClickOutside(buttonRef, () => setIsOpen(false));

const styleClasses = {
mainButtonClasses: '',
dropdownSideButtonClasses: '',
dropdownClasses: '',
};

switch (buttonType) {
case 'ghost':
styleClasses.mainButtonClasses =
'text-white bg-transparent border border-gray-600 hover:border-gray-200 focus:border-gray-100 active:border-gray-100';
styleClasses.dropdownSideButtonClasses =
'bg-transparent border border-gray-600 hover:border-gray-200 focus:border-gray-100 active:border-gray-100';
styleClasses.dropdownClasses = 'bg-gray-700';
break;
default:
styleClasses.mainButtonClasses =
'text-white bg-indigo-600 hover:text-white hover:bg-indigo-500 active:bg-indigo-700 focus:ring-blue';
styleClasses.dropdownSideButtonClasses =
'bg-indigo-700 border border-indigo-600 hover:bg-indigo-500 active:bg-indigo-700 focus:ring-blue';
styleClasses.dropdownClasses = 'bg-indigo-600';
}

return (
<span className="relative z-0 inline-flex h-full rounded-md shadow-sm">
<button
type="button"
className={`relative inline-flex h-full items-center px-4 py-2 text-white bg-indigo-600 hover:bg-indigo-500 text-sm leading-5 font-medium hover:text-white focus:ring-indigo active:bg-indigo-700 focus:z-10 focus:outline-none focus:ring-blue transition ease-in-out duration-150 ${
children ? 'rounded-l-md' : 'rounded-md'
} ${className}`}
className={`relative inline-flex h-full items-center px-4 py-2 text-sm leading-5 font-medium z-10 hover:z-20 focus:z-20 focus:outline-none transition ease-in-out duration-150 ${
styleClasses.mainButtonClasses
} ${children ? 'rounded-l-md' : 'rounded-md'} ${className}`}
ref={buttonRef}
{...props}
>
{text}
</button>
<span className="relative block -ml-px">
<span className="relative z-10 block -ml-px">
{children && (
<button
type="button"
className="relative inline-flex items-center h-full px-2 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out bg-indigo-700 border border-indigo-600 rounded-r-md hover:bg-indigo-500 focus:z-10 focus:outline-none active:bg-indigo-700 focus:ring-blue"
className={`relative inline-flex items-center h-full px-2 py-2 text-sm font-medium leading-5 text-white transition duration-150 ease-in-out rounded-r-md focus:z-10 ${styleClasses.dropdownSideButtonClasses}`}
aria-label="Expand"
onClick={() => setIsOpen((state) => !state)}
>
Expand Down Expand Up @@ -86,7 +128,9 @@ const ButtonWithDropdown: React.FC<ButtonWithDropdownProps> = ({
leaveTo="transform opacity-0 scale-95"
>
<div className="absolute right-0 w-56 mt-2 -mr-1 origin-top-right rounded-md shadow-lg">
<div className="bg-indigo-600 rounded-md ring-1 ring-black ring-opacity-5">
<div
className={`rounded-md ring-1 ring-black ring-opacity-5 ${styleClasses.dropdownClasses}`}
>
<div className="py-1">{children}</div>
</div>
</div>
Expand Down
13 changes: 13 additions & 0 deletions src/components/ExternalLinkBlock/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,35 @@ import React from 'react';
import TmdbLogo from '../../assets/services/tmdb.svg';
import ImdbLogo from '../../assets/services/imdb.svg';
import RTLogo from '../../assets/services/rt.svg';
import PlexLogo from '../../assets/services/plex.svg';

interface ExternalLinkBlockProps {
mediaType: 'movie' | 'tv';
imdbId?: string;
tmdbId?: number;
rtUrl?: string;
plexUrl?: string;
}

const ExternalLinkBlock: React.FC<ExternalLinkBlockProps> = ({
imdbId,
tmdbId,
rtUrl,
mediaType,
plexUrl,
}) => {
return (
<div className="flex justify-end items-center">
{plexUrl && (
<a
href={plexUrl}
className="w-8 mx-2 opacity-50 hover:opacity-100 transition duration-300"
target="_blank"
rel="noreferrer"
>
<PlexLogo />
</a>
)}
{tmdbId && (
<a
href={`https://www.themoviedb.org/${mediaType}/${tmdbId}`}
Expand Down

0 comments on commit 1d7150c

Please sign in to comment.