commit
ac2052f43d
@ -1,71 +0,0 @@
|
|||||||
import React, { FunctionComponent, useMemo } from "react";
|
|
||||||
import { Redirect, Route, Switch, useHistory } from "react-router-dom";
|
|
||||||
import { useDidMount } from "rooks";
|
|
||||||
import { useIsRadarrEnabled, useIsSonarrEnabled } from "../@redux/hooks/site";
|
|
||||||
import BlacklistRouter from "../Blacklist/Router";
|
|
||||||
import DisplayItemRouter from "../DisplayItem/Router";
|
|
||||||
import HistoryRouter from "../History/Router";
|
|
||||||
import SettingRouter from "../Settings/Router";
|
|
||||||
import EmptyPage, { RouterEmptyPath } from "../special-pages/404";
|
|
||||||
import SystemRouter from "../System/Router";
|
|
||||||
import { ScrollToTop } from "../utilities";
|
|
||||||
import WantedRouter from "../Wanted/Router";
|
|
||||||
|
|
||||||
const Router: FunctionComponent<{ className?: string }> = ({ className }) => {
|
|
||||||
const sonarr = useIsSonarrEnabled();
|
|
||||||
const radarr = useIsRadarrEnabled();
|
|
||||||
const redirectPath = useMemo(() => {
|
|
||||||
if (sonarr) {
|
|
||||||
return "/series";
|
|
||||||
} else if (radarr) {
|
|
||||||
return "/movies";
|
|
||||||
} else {
|
|
||||||
return "/settings";
|
|
||||||
}
|
|
||||||
}, [sonarr, radarr]);
|
|
||||||
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
useDidMount(() => {
|
|
||||||
history.listen(() => {
|
|
||||||
// This is a hack to make sure ScrollToTop will be triggered in the next frame (When everything are loaded)
|
|
||||||
setTimeout(ScrollToTop);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/">
|
|
||||||
<Redirect exact to={redirectPath}></Redirect>
|
|
||||||
</Route>
|
|
||||||
<Route path={["/series", "/movies"]}>
|
|
||||||
<DisplayItemRouter></DisplayItemRouter>
|
|
||||||
</Route>
|
|
||||||
<Route path="/wanted">
|
|
||||||
<WantedRouter></WantedRouter>
|
|
||||||
</Route>
|
|
||||||
<Route path="/history">
|
|
||||||
<HistoryRouter></HistoryRouter>
|
|
||||||
</Route>
|
|
||||||
<Route path="/blacklist">
|
|
||||||
<BlacklistRouter></BlacklistRouter>
|
|
||||||
</Route>
|
|
||||||
<Route path="/settings">
|
|
||||||
<SettingRouter></SettingRouter>
|
|
||||||
</Route>
|
|
||||||
<Route path="/system">
|
|
||||||
<SystemRouter></SystemRouter>
|
|
||||||
</Route>
|
|
||||||
<Route exact path={RouterEmptyPath}>
|
|
||||||
<EmptyPage></EmptyPage>
|
|
||||||
</Route>
|
|
||||||
<Route path="*">
|
|
||||||
<Redirect to={RouterEmptyPath}></Redirect>
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Router;
|
|
@ -1,36 +0,0 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
|
||||||
import { Redirect, Route, Switch } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
useIsRadarrEnabled,
|
|
||||||
useIsSonarrEnabled,
|
|
||||||
useSetSidebar,
|
|
||||||
} from "../@redux/hooks/site";
|
|
||||||
import { RouterEmptyPath } from "../special-pages/404";
|
|
||||||
import BlacklistMovies from "./Movies";
|
|
||||||
import BlacklistSeries from "./Series";
|
|
||||||
|
|
||||||
const Router: FunctionComponent = () => {
|
|
||||||
const sonarr = useIsSonarrEnabled();
|
|
||||||
const radarr = useIsRadarrEnabled();
|
|
||||||
|
|
||||||
useSetSidebar("Blacklist");
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
{sonarr && (
|
|
||||||
<Route exact path="/blacklist/series">
|
|
||||||
<BlacklistSeries></BlacklistSeries>
|
|
||||||
</Route>
|
|
||||||
)}
|
|
||||||
{radarr && (
|
|
||||||
<Route path="/blacklist/movies">
|
|
||||||
<BlacklistMovies></BlacklistMovies>
|
|
||||||
</Route>
|
|
||||||
)}
|
|
||||||
<Route path="/blacklist/*">
|
|
||||||
<Redirect to={RouterEmptyPath}></Redirect>
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Router;
|
|
@ -1,45 +0,0 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
|
||||||
import { Redirect, Route, Switch } from "react-router-dom";
|
|
||||||
import { useIsRadarrEnabled, useIsSonarrEnabled } from "../@redux/hooks";
|
|
||||||
import { RouterEmptyPath } from "../special-pages/404";
|
|
||||||
import Episodes from "./Episodes";
|
|
||||||
import MovieDetail from "./MovieDetail";
|
|
||||||
import Movies from "./Movies";
|
|
||||||
import Series from "./Series";
|
|
||||||
|
|
||||||
interface Props {}
|
|
||||||
|
|
||||||
const Router: FunctionComponent<Props> = () => {
|
|
||||||
const radarr = useIsRadarrEnabled();
|
|
||||||
const sonarr = useIsSonarrEnabled();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
{radarr && (
|
|
||||||
<Route exact path="/movies">
|
|
||||||
<Movies></Movies>
|
|
||||||
</Route>
|
|
||||||
)}
|
|
||||||
{radarr && (
|
|
||||||
<Route path="/movies/:id">
|
|
||||||
<MovieDetail></MovieDetail>
|
|
||||||
</Route>
|
|
||||||
)}
|
|
||||||
{sonarr && (
|
|
||||||
<Route exact path="/series">
|
|
||||||
<Series></Series>
|
|
||||||
</Route>
|
|
||||||
)}
|
|
||||||
{sonarr && (
|
|
||||||
<Route path="/series/:id">
|
|
||||||
<Episodes></Episodes>
|
|
||||||
</Route>
|
|
||||||
)}
|
|
||||||
<Route path="*">
|
|
||||||
<Redirect to={RouterEmptyPath}></Redirect>
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Router;
|
|
@ -1,40 +0,0 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
|
||||||
import { Redirect, Route, Switch } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
useIsRadarrEnabled,
|
|
||||||
useIsSonarrEnabled,
|
|
||||||
useSetSidebar,
|
|
||||||
} from "../@redux/hooks/site";
|
|
||||||
import { RouterEmptyPath } from "../special-pages/404";
|
|
||||||
import MoviesHistory from "./Movies";
|
|
||||||
import SeriesHistory from "./Series";
|
|
||||||
import HistoryStats from "./Statistics";
|
|
||||||
|
|
||||||
const Router: FunctionComponent = () => {
|
|
||||||
const sonarr = useIsSonarrEnabled();
|
|
||||||
const radarr = useIsRadarrEnabled();
|
|
||||||
|
|
||||||
useSetSidebar("History");
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
{sonarr && (
|
|
||||||
<Route exact path="/history/series">
|
|
||||||
<SeriesHistory></SeriesHistory>
|
|
||||||
</Route>
|
|
||||||
)}
|
|
||||||
{radarr && (
|
|
||||||
<Route exact path="/history/movies">
|
|
||||||
<MoviesHistory></MoviesHistory>
|
|
||||||
</Route>
|
|
||||||
)}
|
|
||||||
<Route exact path="/history/stats">
|
|
||||||
<HistoryStats></HistoryStats>
|
|
||||||
</Route>
|
|
||||||
<Route path="/history/*">
|
|
||||||
<Redirect to={RouterEmptyPath}></Redirect>
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Router;
|
|
@ -0,0 +1,19 @@
|
|||||||
|
import { FunctionComponent } from "react";
|
||||||
|
import { Redirect } from "react-router-dom";
|
||||||
|
import { useIsRadarrEnabled, useIsSonarrEnabled } from "../@redux/hooks";
|
||||||
|
|
||||||
|
const RootRedirect: FunctionComponent = () => {
|
||||||
|
const sonarr = useIsSonarrEnabled();
|
||||||
|
const radarr = useIsRadarrEnabled();
|
||||||
|
|
||||||
|
let path = "/settings";
|
||||||
|
if (sonarr) {
|
||||||
|
path = "/series";
|
||||||
|
} else if (radarr) {
|
||||||
|
path = "movies";
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Redirect to={path}></Redirect>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RootRedirect;
|
@ -0,0 +1,245 @@
|
|||||||
|
import {
|
||||||
|
faClock,
|
||||||
|
faCogs,
|
||||||
|
faExclamationTriangle,
|
||||||
|
faFileExcel,
|
||||||
|
faFilm,
|
||||||
|
faLaptop,
|
||||||
|
faPlay,
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useIsRadarrEnabled, useIsSonarrEnabled } from "../@redux/hooks";
|
||||||
|
import { useReduxStore } from "../@redux/hooks/base";
|
||||||
|
import BlacklistMoviesView from "../Blacklist/Movies";
|
||||||
|
import BlacklistSeriesView from "../Blacklist/Series";
|
||||||
|
import Episodes from "../DisplayItem/Episodes";
|
||||||
|
import MovieDetail from "../DisplayItem/MovieDetail";
|
||||||
|
import MovieView from "../DisplayItem/Movies";
|
||||||
|
import SeriesView from "../DisplayItem/Series";
|
||||||
|
import MoviesHistoryView from "../History/Movies";
|
||||||
|
import SeriesHistoryView from "../History/Series";
|
||||||
|
import HistoryStats from "../History/Statistics";
|
||||||
|
import SettingsGeneralView from "../Settings/General";
|
||||||
|
import SettingsLanguagesView from "../Settings/Languages";
|
||||||
|
import SettingsNotificationsView from "../Settings/Notifications";
|
||||||
|
import SettingsProvidersView from "../Settings/Providers";
|
||||||
|
import SettingsRadarrView from "../Settings/Radarr";
|
||||||
|
import SettingsSchedulerView from "../Settings/Scheduler";
|
||||||
|
import SettingsSonarrView from "../Settings/Sonarr";
|
||||||
|
import SettingsSubtitlesView from "../Settings/Subtitles";
|
||||||
|
import SettingsUIView from "../Settings/UI";
|
||||||
|
import EmptyPage, { RouterEmptyPath } from "../special-pages/404";
|
||||||
|
import SystemLogsView from "../System/Logs";
|
||||||
|
import SystemProvidersView from "../System/Providers";
|
||||||
|
import SystemReleasesView from "../System/Releases";
|
||||||
|
import SystemStatusView from "../System/Status";
|
||||||
|
import SystemTasksView from "../System/Tasks";
|
||||||
|
import WantedMoviesView from "../Wanted/Movies";
|
||||||
|
import WantedSeriesView from "../Wanted/Series";
|
||||||
|
import { Navigation } from "./nav";
|
||||||
|
import RootRedirect from "./RootRedirect";
|
||||||
|
|
||||||
|
export function useNavigationItems() {
|
||||||
|
const sonarr = useIsSonarrEnabled();
|
||||||
|
const radarr = useIsRadarrEnabled();
|
||||||
|
const { movies, episodes, providers } = useReduxStore((s) => s.site.badges);
|
||||||
|
|
||||||
|
const items = useMemo<Navigation.RouteItem[]>(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
name: "404",
|
||||||
|
path: RouterEmptyPath,
|
||||||
|
component: EmptyPage,
|
||||||
|
routeOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Redirect",
|
||||||
|
path: "/",
|
||||||
|
component: RootRedirect,
|
||||||
|
routeOnly: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faPlay,
|
||||||
|
name: "Series",
|
||||||
|
path: "/series",
|
||||||
|
component: SeriesView,
|
||||||
|
enabled: sonarr,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: "Episode",
|
||||||
|
path: "/:id",
|
||||||
|
component: Episodes,
|
||||||
|
routeOnly: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faFilm,
|
||||||
|
name: "Movies",
|
||||||
|
path: "/movies",
|
||||||
|
component: MovieView,
|
||||||
|
enabled: radarr,
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: "Movie Details",
|
||||||
|
path: "/:id",
|
||||||
|
component: MovieDetail,
|
||||||
|
routeOnly: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faClock,
|
||||||
|
name: "History",
|
||||||
|
path: "/history",
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: "Series",
|
||||||
|
path: "/series",
|
||||||
|
enabled: sonarr,
|
||||||
|
component: SeriesHistoryView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Movies",
|
||||||
|
path: "/movies",
|
||||||
|
enabled: radarr,
|
||||||
|
component: MoviesHistoryView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Statistics",
|
||||||
|
path: "/stats",
|
||||||
|
component: HistoryStats,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faFileExcel,
|
||||||
|
name: "Blacklist",
|
||||||
|
path: "/blacklist",
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: "Series",
|
||||||
|
path: "/series",
|
||||||
|
enabled: sonarr,
|
||||||
|
component: BlacklistSeriesView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Movies",
|
||||||
|
path: "/movies",
|
||||||
|
enabled: radarr,
|
||||||
|
component: BlacklistMoviesView,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faExclamationTriangle,
|
||||||
|
name: "Wanted",
|
||||||
|
path: "/wanted",
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: "Series",
|
||||||
|
path: "/series",
|
||||||
|
badge: episodes,
|
||||||
|
enabled: sonarr,
|
||||||
|
component: WantedSeriesView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Movies",
|
||||||
|
path: "/movies",
|
||||||
|
badge: movies,
|
||||||
|
enabled: radarr,
|
||||||
|
component: WantedMoviesView,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faCogs,
|
||||||
|
name: "Settings",
|
||||||
|
path: "/settings",
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: "General",
|
||||||
|
path: "/general",
|
||||||
|
component: SettingsGeneralView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Languages",
|
||||||
|
path: "/languages",
|
||||||
|
component: SettingsLanguagesView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Providers",
|
||||||
|
path: "/providers",
|
||||||
|
component: SettingsProvidersView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Subtitles",
|
||||||
|
path: "/subtitles",
|
||||||
|
component: SettingsSubtitlesView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sonarr",
|
||||||
|
path: "/sonarr",
|
||||||
|
component: SettingsSonarrView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Radarr",
|
||||||
|
path: "/radarr",
|
||||||
|
component: SettingsRadarrView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Notifications",
|
||||||
|
path: "/notifications",
|
||||||
|
component: SettingsNotificationsView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Scheduler",
|
||||||
|
path: "/scheduler",
|
||||||
|
component: SettingsSchedulerView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "UI",
|
||||||
|
path: "/ui",
|
||||||
|
component: SettingsUIView,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: faLaptop,
|
||||||
|
name: "System",
|
||||||
|
path: "/system",
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
name: "Tasks",
|
||||||
|
path: "/tasks",
|
||||||
|
component: SystemTasksView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Logs",
|
||||||
|
path: "/logs",
|
||||||
|
component: SystemLogsView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Providers",
|
||||||
|
path: "/providers",
|
||||||
|
badge: providers,
|
||||||
|
component: SystemProvidersView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Status",
|
||||||
|
path: "/status",
|
||||||
|
component: SystemStatusView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Releases",
|
||||||
|
path: "/releases",
|
||||||
|
component: SystemReleasesView,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[episodes, movies, providers, radarr, sonarr]
|
||||||
|
);
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FunctionComponent } from "react";
|
||||||
|
|
||||||
|
export declare namespace Navigation {
|
||||||
|
type RouteWithoutChild = {
|
||||||
|
icon?: IconDefinition;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
component: FunctionComponent;
|
||||||
|
badge?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
routeOnly?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RouteWithChild = {
|
||||||
|
icon: IconDefinition;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
component?: FunctionComponent;
|
||||||
|
badge?: number;
|
||||||
|
enabled?: boolean;
|
||||||
|
routes: RouteWithoutChild[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type RouteItem = RouteWithChild | RouteWithoutChild;
|
||||||
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
import { FunctionComponent } from "react";
|
||||||
|
import { Redirect, Route, Switch, useHistory } from "react-router";
|
||||||
|
import { useDidMount } from "rooks";
|
||||||
|
import { useNavigationItems } from "../Navigation";
|
||||||
|
import { Navigation } from "../Navigation/nav";
|
||||||
|
import { RouterEmptyPath } from "../special-pages/404";
|
||||||
|
import { BuildKey, ScrollToTop } from "../utilities";
|
||||||
|
|
||||||
|
const Router: FunctionComponent = () => {
|
||||||
|
const navItems = useNavigationItems();
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
useDidMount(() => {
|
||||||
|
history.listen(() => {
|
||||||
|
// This is a hack to make sure ScrollToTop will be triggered in the next frame (When everything are loaded)
|
||||||
|
setTimeout(ScrollToTop);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="d-flex flex-row flex-grow-1 main-router">
|
||||||
|
<Switch>
|
||||||
|
{navItems.map((v, idx) => {
|
||||||
|
if ("routes" in v) {
|
||||||
|
return (
|
||||||
|
<Route path={v.path} key={BuildKey(idx, v.name, "router")}>
|
||||||
|
<ParentRouter {...v}></ParentRouter>
|
||||||
|
</Route>
|
||||||
|
);
|
||||||
|
} else if (v.enabled !== false) {
|
||||||
|
return (
|
||||||
|
<Route
|
||||||
|
key={BuildKey(idx, v.name, "root")}
|
||||||
|
exact
|
||||||
|
path={v.path}
|
||||||
|
component={v.component}
|
||||||
|
></Route>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
<Route path="*">
|
||||||
|
<Redirect to={RouterEmptyPath}></Redirect>
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Router;
|
||||||
|
|
||||||
|
const ParentRouter: FunctionComponent<Navigation.RouteWithChild> = ({
|
||||||
|
path,
|
||||||
|
enabled,
|
||||||
|
component,
|
||||||
|
routes,
|
||||||
|
}) => {
|
||||||
|
if (enabled === false || (component === undefined && routes.length === 0)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const ParentComponent =
|
||||||
|
component ?? (() => <Redirect to={path + routes[0].path}></Redirect>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Route exact path={path} component={ParentComponent}></Route>
|
||||||
|
{routes
|
||||||
|
.filter((v) => v.enabled !== false)
|
||||||
|
.map((v, idx) => (
|
||||||
|
<Route
|
||||||
|
key={BuildKey(idx, v.name, "route")}
|
||||||
|
exact
|
||||||
|
path={path + v.path}
|
||||||
|
component={v.component}
|
||||||
|
></Route>
|
||||||
|
))}
|
||||||
|
<Route path="*">
|
||||||
|
<Redirect to={RouterEmptyPath}></Redirect>
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
};
|
@ -1,58 +0,0 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
|
||||||
import { Redirect, Route, Switch } from "react-router-dom";
|
|
||||||
import { useSetSidebar } from "../@redux/hooks/site";
|
|
||||||
import { RouterEmptyPath } from "../special-pages/404";
|
|
||||||
import General from "./General";
|
|
||||||
import Languages from "./Languages";
|
|
||||||
import Notifications from "./Notifications";
|
|
||||||
import Providers from "./Providers";
|
|
||||||
import Radarr from "./Radarr";
|
|
||||||
import Scheduler from "./Scheduler";
|
|
||||||
import Sonarr from "./Sonarr";
|
|
||||||
import Subtitles from "./Subtitles";
|
|
||||||
import UI from "./UI";
|
|
||||||
|
|
||||||
interface Props {}
|
|
||||||
|
|
||||||
const Router: FunctionComponent<Props> = () => {
|
|
||||||
useSetSidebar("Settings");
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/settings">
|
|
||||||
<Redirect exact to="/settings/general"></Redirect>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/settings/general">
|
|
||||||
<General></General>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/settings/ui">
|
|
||||||
<UI></UI>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/settings/sonarr">
|
|
||||||
<Sonarr></Sonarr>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/settings/radarr">
|
|
||||||
<Radarr></Radarr>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/settings/languages">
|
|
||||||
<Languages></Languages>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/settings/subtitles">
|
|
||||||
<Subtitles></Subtitles>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/settings/scheduler">
|
|
||||||
<Scheduler></Scheduler>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/settings/providers">
|
|
||||||
<Providers></Providers>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/settings/notifications">
|
|
||||||
<Notifications></Notifications>
|
|
||||||
</Route>
|
|
||||||
<Route path="/settings/*">
|
|
||||||
<Redirect to={RouterEmptyPath}></Redirect>
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Router;
|
|
@ -1,179 +0,0 @@
|
|||||||
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import React, { FunctionComponent, useContext, useMemo } from "react";
|
|
||||||
import { Badge, Collapse, ListGroupItem } from "react-bootstrap";
|
|
||||||
import { NavLink } from "react-router-dom";
|
|
||||||
import { siteChangeSidebar } from "../@redux/actions";
|
|
||||||
import { useReduxAction, useReduxStore } from "../@redux/hooks/base";
|
|
||||||
import { SidebarToggleContext } from "../App";
|
|
||||||
import {
|
|
||||||
BadgeProvider,
|
|
||||||
ChildBadgeProvider,
|
|
||||||
CollapseItemType,
|
|
||||||
LinkItemType,
|
|
||||||
} from "./types";
|
|
||||||
|
|
||||||
export const HiddenKeysContext = React.createContext<string[]>([]);
|
|
||||||
|
|
||||||
export const BadgesContext = React.createContext<BadgeProvider>({});
|
|
||||||
|
|
||||||
function useToggleSidebar() {
|
|
||||||
return useReduxAction(siteChangeSidebar);
|
|
||||||
}
|
|
||||||
|
|
||||||
function useSidebarKey() {
|
|
||||||
return useReduxStore((s) => s.site.sidebar);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LinkItem: FunctionComponent<LinkItemType> = ({
|
|
||||||
link,
|
|
||||||
name,
|
|
||||||
icon,
|
|
||||||
}) => {
|
|
||||||
const badges = useContext(BadgesContext);
|
|
||||||
const toggle = useContext(SidebarToggleContext);
|
|
||||||
|
|
||||||
const badgeValue = useMemo(() => {
|
|
||||||
let badge: Nullable<number> = null;
|
|
||||||
if (name in badges) {
|
|
||||||
let item = badges[name];
|
|
||||||
if (typeof item === "number") {
|
|
||||||
badge = item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return badge;
|
|
||||||
}, [badges, name]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NavLink
|
|
||||||
activeClassName="sb-active"
|
|
||||||
className="list-group-item list-group-item-action sidebar-button"
|
|
||||||
to={link}
|
|
||||||
onClick={toggle}
|
|
||||||
>
|
|
||||||
<DisplayItem
|
|
||||||
badge={badgeValue ?? undefined}
|
|
||||||
name={name}
|
|
||||||
icon={icon}
|
|
||||||
></DisplayItem>
|
|
||||||
</NavLink>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CollapseItem: FunctionComponent<CollapseItemType> = ({
|
|
||||||
icon,
|
|
||||||
name,
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const badges = useContext(BadgesContext);
|
|
||||||
const hiddenKeys = useContext(HiddenKeysContext);
|
|
||||||
const toggleSidebar = useContext(SidebarToggleContext);
|
|
||||||
|
|
||||||
const sidebarKey = useSidebarKey();
|
|
||||||
const updateSidebar = useToggleSidebar();
|
|
||||||
|
|
||||||
const [badgeValue, childValue] = useMemo<
|
|
||||||
[Nullable<number>, Nullable<ChildBadgeProvider>]
|
|
||||||
>(() => {
|
|
||||||
let badge: Nullable<number> = null;
|
|
||||||
let child: Nullable<ChildBadgeProvider> = null;
|
|
||||||
|
|
||||||
if (name in badges) {
|
|
||||||
const item = badges[name];
|
|
||||||
if (typeof item === "number") {
|
|
||||||
badge = item;
|
|
||||||
} else if (typeof item === "object") {
|
|
||||||
badge = 0;
|
|
||||||
child = item;
|
|
||||||
for (const it in item) {
|
|
||||||
badge += item[it];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [badge, child];
|
|
||||||
}, [badges, name]);
|
|
||||||
|
|
||||||
const active = useMemo(() => sidebarKey === name, [sidebarKey, name]);
|
|
||||||
|
|
||||||
const collapseBoxClass = useMemo(
|
|
||||||
() => `sidebar-collapse-box ${active ? "active" : ""}`,
|
|
||||||
[active]
|
|
||||||
);
|
|
||||||
|
|
||||||
const childrenElems = useMemo(
|
|
||||||
() =>
|
|
||||||
children
|
|
||||||
.filter((v) => !hiddenKeys.includes(v.hiddenKey ?? ""))
|
|
||||||
.map((ch) => {
|
|
||||||
let badge: Nullable<number> = null;
|
|
||||||
if (childValue && ch.name in childValue) {
|
|
||||||
badge = childValue[ch.name];
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<NavLink
|
|
||||||
key={ch.name}
|
|
||||||
activeClassName="sb-active"
|
|
||||||
className="list-group-item list-group-item-action sidebar-button sb-collapse"
|
|
||||||
to={ch.link}
|
|
||||||
onClick={toggleSidebar}
|
|
||||||
>
|
|
||||||
<DisplayItem
|
|
||||||
badge={badge === 0 ? undefined : badge ?? undefined}
|
|
||||||
name={ch.name}
|
|
||||||
></DisplayItem>
|
|
||||||
</NavLink>
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
[children, hiddenKeys, childValue, toggleSidebar]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (childrenElems.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={collapseBoxClass}>
|
|
||||||
<ListGroupItem
|
|
||||||
action
|
|
||||||
className="sidebar-button"
|
|
||||||
onClick={() => {
|
|
||||||
if (active) {
|
|
||||||
updateSidebar("");
|
|
||||||
} else {
|
|
||||||
updateSidebar(name);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DisplayItem
|
|
||||||
badge={badgeValue === 0 ? undefined : badgeValue ?? undefined}
|
|
||||||
icon={icon}
|
|
||||||
name={name}
|
|
||||||
></DisplayItem>
|
|
||||||
</ListGroupItem>
|
|
||||||
<Collapse in={active}>
|
|
||||||
<div className="sidebar-collapse">{childrenElems}</div>
|
|
||||||
</Collapse>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface DisplayProps {
|
|
||||||
name: string;
|
|
||||||
icon?: IconDefinition;
|
|
||||||
badge?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DisplayItem: FunctionComponent<DisplayProps> = ({
|
|
||||||
name,
|
|
||||||
icon,
|
|
||||||
badge,
|
|
||||||
}) => (
|
|
||||||
<React.Fragment>
|
|
||||||
{icon && (
|
|
||||||
<FontAwesomeIcon size="1x" className="icon" icon={icon}></FontAwesomeIcon>
|
|
||||||
)}
|
|
||||||
<span className="d-flex flex-grow-1 justify-content-between">
|
|
||||||
{name} <Badge variant="secondary">{badge}</Badge>
|
|
||||||
</span>
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
@ -1,148 +0,0 @@
|
|||||||
import {
|
|
||||||
faClock,
|
|
||||||
faCogs,
|
|
||||||
faExclamationTriangle,
|
|
||||||
faFileExcel,
|
|
||||||
faFilm,
|
|
||||||
faLaptop,
|
|
||||||
faPlay,
|
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { SidebarDefinition } from "./types";
|
|
||||||
|
|
||||||
export const SonarrDisabledKey = "sonarr-disabled";
|
|
||||||
export const RadarrDisabledKey = "radarr-disabled";
|
|
||||||
|
|
||||||
export const SidebarList: SidebarDefinition[] = [
|
|
||||||
{
|
|
||||||
icon: faPlay,
|
|
||||||
name: "Series",
|
|
||||||
link: "/series",
|
|
||||||
hiddenKey: SonarrDisabledKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: faFilm,
|
|
||||||
name: "Movies",
|
|
||||||
link: "/movies",
|
|
||||||
hiddenKey: RadarrDisabledKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: faClock,
|
|
||||||
name: "History",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: "Series",
|
|
||||||
link: "/history/series",
|
|
||||||
hiddenKey: SonarrDisabledKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Movies",
|
|
||||||
link: "/history/movies",
|
|
||||||
hiddenKey: RadarrDisabledKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Statistics",
|
|
||||||
link: "/history/stats",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: faFileExcel,
|
|
||||||
name: "Blacklist",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: "Series",
|
|
||||||
link: "/blacklist/series",
|
|
||||||
hiddenKey: SonarrDisabledKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Movies",
|
|
||||||
link: "/blacklist/movies",
|
|
||||||
hiddenKey: RadarrDisabledKey,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: faExclamationTriangle,
|
|
||||||
name: "Wanted",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: "Series",
|
|
||||||
link: "/wanted/series",
|
|
||||||
hiddenKey: SonarrDisabledKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Movies",
|
|
||||||
link: "/wanted/movies",
|
|
||||||
hiddenKey: RadarrDisabledKey,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: faCogs,
|
|
||||||
name: "Settings",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: "General",
|
|
||||||
link: "/settings/general",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Languages",
|
|
||||||
link: "/settings/languages",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Providers",
|
|
||||||
link: "/settings/providers",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Subtitles",
|
|
||||||
link: "/settings/subtitles",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Sonarr",
|
|
||||||
link: "/settings/sonarr",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Radarr",
|
|
||||||
link: "/settings/radarr",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Notifications",
|
|
||||||
link: "/settings/notifications",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Scheduler",
|
|
||||||
link: "/settings/scheduler",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "UI",
|
|
||||||
link: "/settings/ui",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: faLaptop,
|
|
||||||
name: "System",
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
name: "Tasks",
|
|
||||||
link: "/system/tasks",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Logs",
|
|
||||||
link: "/system/logs",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Providers",
|
|
||||||
link: "/system/providers",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Status",
|
|
||||||
link: "/system/status",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Releases",
|
|
||||||
link: "/system/releases",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
@ -1,29 +0,0 @@
|
|||||||
import { IconDefinition } from "@fortawesome/fontawesome-common-types";
|
|
||||||
|
|
||||||
type SidebarDefinition = LinkItemType | CollapseItemType;
|
|
||||||
|
|
||||||
type BaseSidebar = {
|
|
||||||
icon: IconDefinition;
|
|
||||||
name: string;
|
|
||||||
hiddenKey?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type LinkItemType = BaseSidebar & {
|
|
||||||
link: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CollapseItemType = BaseSidebar & {
|
|
||||||
children: {
|
|
||||||
name: string;
|
|
||||||
link: string;
|
|
||||||
hiddenKey?: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type BadgeProvider = {
|
|
||||||
[parent: string]: ChildBadgeProvider | number;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ChildBadgeProvider = {
|
|
||||||
[child: string]: number;
|
|
||||||
};
|
|
@ -1,37 +0,0 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
|
||||||
import { Redirect, Route, Switch } from "react-router-dom";
|
|
||||||
import { useSetSidebar } from "../@redux/hooks/site";
|
|
||||||
import { RouterEmptyPath } from "../special-pages/404";
|
|
||||||
import Logs from "./Logs";
|
|
||||||
import Providers from "./Providers";
|
|
||||||
import Releases from "./Releases";
|
|
||||||
import Status from "./Status";
|
|
||||||
import Tasks from "./Tasks";
|
|
||||||
|
|
||||||
const Router: FunctionComponent = () => {
|
|
||||||
useSetSidebar("System");
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/system/tasks">
|
|
||||||
<Tasks></Tasks>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/system/status">
|
|
||||||
<Status></Status>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/system/providers">
|
|
||||||
<Providers></Providers>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/system/logs">
|
|
||||||
<Logs></Logs>
|
|
||||||
</Route>
|
|
||||||
<Route exact path="/system/releases">
|
|
||||||
<Releases></Releases>
|
|
||||||
</Route>
|
|
||||||
<Route path="/system/*">
|
|
||||||
<Redirect to={RouterEmptyPath}></Redirect>
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Router;
|
|
@ -1,36 +0,0 @@
|
|||||||
import React, { FunctionComponent } from "react";
|
|
||||||
import { Redirect, Route, Switch } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
useIsRadarrEnabled,
|
|
||||||
useIsSonarrEnabled,
|
|
||||||
useSetSidebar,
|
|
||||||
} from "../@redux/hooks/site";
|
|
||||||
import { RouterEmptyPath } from "../special-pages/404";
|
|
||||||
import Movies from "./Movies";
|
|
||||||
import Series from "./Series";
|
|
||||||
|
|
||||||
const Router: FunctionComponent = () => {
|
|
||||||
const sonarr = useIsSonarrEnabled();
|
|
||||||
const radarr = useIsRadarrEnabled();
|
|
||||||
|
|
||||||
useSetSidebar("Wanted");
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
{sonarr && (
|
|
||||||
<Route exact path="/wanted/series">
|
|
||||||
<Series></Series>
|
|
||||||
</Route>
|
|
||||||
)}
|
|
||||||
{radarr && (
|
|
||||||
<Route exact path="/wanted/movies">
|
|
||||||
<Movies></Movies>
|
|
||||||
</Route>
|
|
||||||
)}
|
|
||||||
<Route path="/wanted/*">
|
|
||||||
<Redirect to={RouterEmptyPath}></Redirect>
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Router;
|
|
@ -0,0 +1,452 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import io
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
from subzero.language import Language
|
||||||
|
from guessit import guessit
|
||||||
|
from requests import Session
|
||||||
|
|
||||||
|
from subliminal.providers import ParserBeautifulSoup
|
||||||
|
from subliminal_patch.providers import Provider
|
||||||
|
from subliminal_patch.subtitle import Subtitle
|
||||||
|
from subliminal.subtitle import fix_line_ending
|
||||||
|
from subliminal import __short_version__
|
||||||
|
from subliminal.cache import SHOW_EXPIRATION_TIME, region
|
||||||
|
from subliminal.exceptions import AuthenticationError, ConfigurationError
|
||||||
|
from subliminal_patch.subtitle import guess_matches
|
||||||
|
from subliminal_patch.utils import sanitize
|
||||||
|
from subliminal.video import Episode, Movie
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class KtuvitSubtitle(Subtitle):
|
||||||
|
"""Ktuvit Subtitle."""
|
||||||
|
|
||||||
|
provider_name = "ktuvit"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
language,
|
||||||
|
hearing_impaired,
|
||||||
|
page_link,
|
||||||
|
series,
|
||||||
|
season,
|
||||||
|
episode,
|
||||||
|
title,
|
||||||
|
imdb_id,
|
||||||
|
ktuvit_id,
|
||||||
|
subtitle_id,
|
||||||
|
release,
|
||||||
|
):
|
||||||
|
super(KtuvitSubtitle, self).__init__(language, hearing_impaired, page_link)
|
||||||
|
self.series = series
|
||||||
|
self.season = season
|
||||||
|
self.episode = episode
|
||||||
|
self.title = title
|
||||||
|
self.imdb_id = imdb_id
|
||||||
|
self.ktuvit_id = ktuvit_id
|
||||||
|
self.subtitle_id = subtitle_id
|
||||||
|
self.release = release
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
return str(self.subtitle_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def release_info(self):
|
||||||
|
return self.release
|
||||||
|
|
||||||
|
def get_matches(self, video):
|
||||||
|
matches = set()
|
||||||
|
# episode
|
||||||
|
if isinstance(video, Episode):
|
||||||
|
# series
|
||||||
|
if video.series and (
|
||||||
|
sanitize(self.title)
|
||||||
|
in (
|
||||||
|
sanitize(name) for name in [video.series] + video.alternative_series
|
||||||
|
)
|
||||||
|
):
|
||||||
|
matches.add("series")
|
||||||
|
# season
|
||||||
|
if video.season and self.season == video.season:
|
||||||
|
matches.add("season")
|
||||||
|
# episode
|
||||||
|
if video.episode and self.episode == video.episode:
|
||||||
|
matches.add("episode")
|
||||||
|
# imdb_id
|
||||||
|
if video.series_imdb_id and self.imdb_id == video.series_imdb_id:
|
||||||
|
matches.add("series_imdb_id")
|
||||||
|
# guess
|
||||||
|
matches |= guess_matches(video, guessit(self.release, {"type": "episode"}))
|
||||||
|
# movie
|
||||||
|
elif isinstance(video, Movie):
|
||||||
|
# guess
|
||||||
|
matches |= guess_matches(video, guessit(self.release, {"type": "movie"}))
|
||||||
|
|
||||||
|
# title
|
||||||
|
if video.title and (
|
||||||
|
sanitize(self.title)
|
||||||
|
in (sanitize(name) for name in [video.title] + video.alternative_titles)
|
||||||
|
):
|
||||||
|
matches.add("title")
|
||||||
|
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
|
class KtuvitProvider(Provider):
|
||||||
|
"""Ktuvit Provider."""
|
||||||
|
|
||||||
|
languages = {Language(l) for l in ["heb"]}
|
||||||
|
server_url = "https://www.ktuvit.me/"
|
||||||
|
sign_in_url = "Services/MembershipService.svc/Login"
|
||||||
|
search_url = "Services/ContentProvider.svc/SearchPage_search"
|
||||||
|
movie_info_url = "MovieInfo.aspx?ID="
|
||||||
|
episode_info_url = "Services/GetModuleAjax.ashx?"
|
||||||
|
request_download_id_url = "Services/ContentProvider.svc/RequestSubtitleDownload"
|
||||||
|
download_link = "Services/DownloadFile.ashx?DownloadIdentifier="
|
||||||
|
subtitle_class = KtuvitSubtitle
|
||||||
|
|
||||||
|
_tmdb_api_key = "a51ee051bcd762543373903de296e0a3"
|
||||||
|
|
||||||
|
def __init__(self, email=None, hashed_password=None):
|
||||||
|
if any((email, hashed_password)) and not all((email, hashed_password)):
|
||||||
|
raise ConfigurationError("Email and Hashed Password must be specified")
|
||||||
|
|
||||||
|
self.email = email
|
||||||
|
self.hashed_password = hashed_password
|
||||||
|
self.logged_in = False
|
||||||
|
self.session = None
|
||||||
|
self.loginCookie = None
|
||||||
|
|
||||||
|
def initialize(self):
|
||||||
|
self.session = Session()
|
||||||
|
|
||||||
|
# login
|
||||||
|
if self.email and self.hashed_password:
|
||||||
|
logger.info("Logging in")
|
||||||
|
|
||||||
|
data = {"request": {"Email": self.email, "Password": self.hashed_password}}
|
||||||
|
|
||||||
|
self.session.headers['Accept-Encoding'] = 'gzip'
|
||||||
|
self.session.headers['Accept-Language'] = 'en-us,en;q=0.5'
|
||||||
|
self.session.headers['Pragma'] = 'no-cache'
|
||||||
|
self.session.headers['Cache-Control'] = 'no-cache'
|
||||||
|
self.session.headers['Content-Type'] = 'application/json'
|
||||||
|
self.session.headers['User-Agent']: os.environ.get("SZ_USER_AGENT", "Sub-Zero/2")
|
||||||
|
|
||||||
|
r = self.session.post(
|
||||||
|
self.server_url + self.sign_in_url,
|
||||||
|
json=data,
|
||||||
|
allow_redirects=False,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
if r.content:
|
||||||
|
try:
|
||||||
|
responseContent = r.json()
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
AuthenticationError("Unable to parse JSON return while authenticating to the provider.")
|
||||||
|
else:
|
||||||
|
isSuccess = False
|
||||||
|
if 'd' in responseContent:
|
||||||
|
responseContent = json.loads(responseContent['d'])
|
||||||
|
isSuccess = responseContent.get('IsSuccess', False)
|
||||||
|
if not isSuccess:
|
||||||
|
AuthenticationError("ErrorMessage: " + responseContent['d'].get("ErrorMessage", "[None]"))
|
||||||
|
else:
|
||||||
|
AuthenticationError("Incomplete JSON returned while authenticating to the provider.")
|
||||||
|
|
||||||
|
logger.debug("Logged in")
|
||||||
|
self.loginCookie = (
|
||||||
|
r.headers["set-cookie"][1].split(";")[0].replace("Login=", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.session.headers["Accept"]="application/json, text/javascript, */*; q=0.01"
|
||||||
|
self.session.headers["Cookie"]="Login=" + self.loginCookie
|
||||||
|
|
||||||
|
self.logged_in = True
|
||||||
|
|
||||||
|
def terminate(self):
|
||||||
|
self.session.close()
|
||||||
|
|
||||||
|
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
|
||||||
|
def _search_imdb_id(self, title, year, is_movie):
|
||||||
|
"""Search the IMDB ID for the given `title` and `year`.
|
||||||
|
|
||||||
|
:param str title: title to search for.
|
||||||
|
:param int year: year to search for (or 0 if not relevant).
|
||||||
|
:param bool is_movie: If True, IMDB ID will be searched for in TMDB instead of Wizdom.
|
||||||
|
:return: the IMDB ID for the given title and year (or None if not found).
|
||||||
|
:rtype: str
|
||||||
|
|
||||||
|
"""
|
||||||
|
# make the search
|
||||||
|
logger.info(
|
||||||
|
"Searching IMDB ID for %r%r",
|
||||||
|
title,
|
||||||
|
"" if not year else " ({})".format(year),
|
||||||
|
)
|
||||||
|
category = "movie" if is_movie else "tv"
|
||||||
|
title = title.replace("'", "")
|
||||||
|
# get TMDB ID first
|
||||||
|
r = self.session.get(
|
||||||
|
"http://api.tmdb.org/3/search/{}?api_key={}&query={}{}&language=en".format(
|
||||||
|
category,
|
||||||
|
self._tmdb_api_key,
|
||||||
|
title,
|
||||||
|
"" if not year else "&year={}".format(year),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
tmdb_results = r.json().get("results")
|
||||||
|
if tmdb_results:
|
||||||
|
tmdb_id = tmdb_results[0].get("id")
|
||||||
|
if tmdb_id:
|
||||||
|
# get actual IMDB ID from TMDB
|
||||||
|
r = self.session.get(
|
||||||
|
"http://api.tmdb.org/3/{}/{}{}?api_key={}&language=en".format(
|
||||||
|
category,
|
||||||
|
tmdb_id,
|
||||||
|
"" if is_movie else "/external_ids",
|
||||||
|
self._tmdb_api_key,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
imdb_id = r.json().get("imdb_id")
|
||||||
|
if imdb_id:
|
||||||
|
return str(imdb_id)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
def query(
|
||||||
|
self, title, season=None, episode=None, year=None, filename=None, imdb_id=None
|
||||||
|
):
|
||||||
|
# search for the IMDB ID if needed.
|
||||||
|
is_movie = not (season and episode)
|
||||||
|
imdb_id = imdb_id or self._search_imdb_id(title, year, is_movie)
|
||||||
|
if not imdb_id:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# search
|
||||||
|
logger.debug("Using IMDB ID %r", imdb_id)
|
||||||
|
|
||||||
|
query = {
|
||||||
|
"FilmName": title,
|
||||||
|
"Actors": [],
|
||||||
|
"Studios": [],
|
||||||
|
"Directors": [],
|
||||||
|
"Genres": [],
|
||||||
|
"Countries": [],
|
||||||
|
"Languages": [],
|
||||||
|
"Year": "",
|
||||||
|
"Rating": [],
|
||||||
|
"Page": 1,
|
||||||
|
"SearchType": "0",
|
||||||
|
"WithSubsOnly": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not is_movie:
|
||||||
|
query["SearchType"] = "1"
|
||||||
|
|
||||||
|
if year:
|
||||||
|
query["Year"] = year
|
||||||
|
|
||||||
|
# get the list of subtitles
|
||||||
|
logger.debug("Getting the list of subtitles")
|
||||||
|
|
||||||
|
url = self.server_url + self.search_url
|
||||||
|
r = self.session.post(
|
||||||
|
url, json={"request": query}, timeout=10
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
if r.content:
|
||||||
|
try:
|
||||||
|
responseContent = r.json()
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
json.decoder.JSONDecodeError("Unable to parse JSON returned while getting Film/Series Information.")
|
||||||
|
else:
|
||||||
|
isSuccess = False
|
||||||
|
if 'd' in responseContent:
|
||||||
|
responseContent = json.loads(responseContent['d'])
|
||||||
|
results = responseContent.get('Films', [])
|
||||||
|
else:
|
||||||
|
json.decoder.JSONDecodeError("Incomplete JSON returned while getting Film/Series Information.")
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
# loop over results
|
||||||
|
subtitles = {}
|
||||||
|
for result in results:
|
||||||
|
imdb_link = result["IMDB_Link"]
|
||||||
|
imdb_link = imdb_link[0: -1] if imdb_link.endswith("/") else imdb_link
|
||||||
|
results_imdb_id = imdb_link.split("/")[-1]
|
||||||
|
|
||||||
|
if results_imdb_id != imdb_id:
|
||||||
|
logger.debug(
|
||||||
|
"Subtitles is for IMDB %r but actual IMDB ID is %r",
|
||||||
|
results_imdb_id,
|
||||||
|
imdb_id,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
language = Language("heb")
|
||||||
|
hearing_impaired = False
|
||||||
|
ktuvit_id = result["ID"]
|
||||||
|
page_link = self.server_url + self.movie_info_url + ktuvit_id
|
||||||
|
|
||||||
|
if is_movie:
|
||||||
|
subs = self._search_movie(ktuvit_id)
|
||||||
|
else:
|
||||||
|
subs = self._search_tvshow(ktuvit_id, season, episode)
|
||||||
|
|
||||||
|
for sub in subs:
|
||||||
|
# otherwise create it
|
||||||
|
subtitle = KtuvitSubtitle(
|
||||||
|
language,
|
||||||
|
hearing_impaired,
|
||||||
|
page_link,
|
||||||
|
title,
|
||||||
|
season,
|
||||||
|
episode,
|
||||||
|
title,
|
||||||
|
imdb_id,
|
||||||
|
ktuvit_id,
|
||||||
|
sub["sub_id"],
|
||||||
|
sub["rls"],
|
||||||
|
)
|
||||||
|
logger.debug("Found subtitle %r", subtitle)
|
||||||
|
subtitles[sub["sub_id"]] = subtitle
|
||||||
|
|
||||||
|
return subtitles.values()
|
||||||
|
|
||||||
|
def _search_tvshow(self, id, season, episode):
|
||||||
|
subs = []
|
||||||
|
|
||||||
|
url = (
|
||||||
|
self.server_url
|
||||||
|
+ self.episode_info_url
|
||||||
|
+ "moduleName=SubtitlesList&SeriesID={}&Season={}&Episode={}".format(
|
||||||
|
id, season, episode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
r = self.session.get(url, timeout=10)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
sub_list = ParserBeautifulSoup(r.content, ["html.parser"])
|
||||||
|
sub_rows = sub_list.find_all("tr")
|
||||||
|
|
||||||
|
for row in sub_rows:
|
||||||
|
columns = row.find_all("td")
|
||||||
|
sub = {"id": id}
|
||||||
|
|
||||||
|
for index, column in enumerate(columns):
|
||||||
|
if index == 0:
|
||||||
|
sub['rls'] = column.get_text().strip().split("\n")[0]
|
||||||
|
if index == 5:
|
||||||
|
sub['sub_id'] = column.find("input", attrs={"data-sub-id": True})["data-sub-id"]
|
||||||
|
|
||||||
|
subs.append(sub)
|
||||||
|
return subs
|
||||||
|
|
||||||
|
def _search_movie(self, movie_id):
|
||||||
|
subs = []
|
||||||
|
url = self.server_url + self.movie_info_url + movie_id
|
||||||
|
r = self.session.get(url, timeout=10)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
html = ParserBeautifulSoup(r.content, ["html.parser"])
|
||||||
|
sub_rows = html.select("table#subtitlesList tbody > tr")
|
||||||
|
|
||||||
|
for row in sub_rows:
|
||||||
|
columns = row.find_all("td")
|
||||||
|
sub = {
|
||||||
|
'id': movie_id
|
||||||
|
}
|
||||||
|
for index, column in enumerate(columns):
|
||||||
|
if index == 0:
|
||||||
|
sub['rls'] = column.get_text().strip().split("\n")[0]
|
||||||
|
if index == 5:
|
||||||
|
sub['sub_id'] = column.find("a", attrs={"data-subtitle-id": True})["data-subtitle-id"]
|
||||||
|
|
||||||
|
subs.append(sub)
|
||||||
|
return subs
|
||||||
|
|
||||||
|
def list_subtitles(self, video, languages):
|
||||||
|
season = episode = None
|
||||||
|
year = video.year
|
||||||
|
filename = video.name
|
||||||
|
imdb_id = video.imdb_id
|
||||||
|
|
||||||
|
if isinstance(video, Episode):
|
||||||
|
titles = [video.series] + video.alternative_series
|
||||||
|
season = video.season
|
||||||
|
episode = video.episode
|
||||||
|
imdb_id = video.series_imdb_id
|
||||||
|
else:
|
||||||
|
titles = [video.title] + video.alternative_titles
|
||||||
|
imdb_id = video.imdb_id
|
||||||
|
|
||||||
|
for title in titles:
|
||||||
|
subtitles = [
|
||||||
|
s
|
||||||
|
for s in self.query(title, season, episode, year, filename, imdb_id)
|
||||||
|
if s.language in languages
|
||||||
|
]
|
||||||
|
if subtitles:
|
||||||
|
return subtitles
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def download_subtitle(self, subtitle):
|
||||||
|
if isinstance(subtitle, KtuvitSubtitle):
|
||||||
|
downloadIdentifierRequest = {
|
||||||
|
"FilmID": subtitle.ktuvit_id,
|
||||||
|
"SubtitleID": subtitle.subtitle_id,
|
||||||
|
"FontSize": 0,
|
||||||
|
"FontColor": "",
|
||||||
|
"PredefinedLayout": -1,
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Download Identifier Request data: " + str(json.dumps({"request": downloadIdentifierRequest})))
|
||||||
|
|
||||||
|
# download
|
||||||
|
url = self.server_url + self.request_download_id_url
|
||||||
|
r = self.session.post(
|
||||||
|
url, json={"request": downloadIdentifierRequest}, timeout=10
|
||||||
|
)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
if r.content:
|
||||||
|
try:
|
||||||
|
responseContent = r.json()
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
json.decoder.JSONDecodeError("Unable to parse JSON returned while getting Download Identifier.")
|
||||||
|
else:
|
||||||
|
isSuccess = False
|
||||||
|
if 'd' in responseContent:
|
||||||
|
responseContent = json.loads(responseContent['d'])
|
||||||
|
downloadIdentifier = responseContent.get('DownloadIdentifier', None)
|
||||||
|
|
||||||
|
if not downloadIdentifier:
|
||||||
|
json.decoder.JSONDecodeError("Missing Download Identifier.")
|
||||||
|
else:
|
||||||
|
json.decoder.JSONDecodeError("Incomplete JSON returned while getting Download Identifier.")
|
||||||
|
|
||||||
|
url = self.server_url + self.download_link + downloadIdentifier
|
||||||
|
|
||||||
|
r = self.session.get(url, timeout=10)
|
||||||
|
r.raise_for_status()
|
||||||
|
|
||||||
|
if not r.content:
|
||||||
|
logger.debug(
|
||||||
|
"Unable to download subtitle. No data returned from provider"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
subtitle.content = fix_line_ending(r.content)
|
Loading…
Reference in new issue