mirror of https://github.com/morpheus65535/bazarr
Merge development into master
This commit is contained in:
commit
ac2052f43d
|
@ -65,7 +65,7 @@ jobs:
|
|||
uses: actions/checkout@v2
|
||||
|
||||
- name: Merge development -> master
|
||||
uses: devmasx/merge-branch@v1.3.1
|
||||
uses: devmasx/merge-branch@1.4.0
|
||||
with:
|
||||
type: now
|
||||
from_branch: development
|
||||
|
|
|
@ -50,6 +50,7 @@ If you need something that is not already part of Bazarr, feel free to create a
|
|||
* Hosszupuska
|
||||
* LegendasDivx
|
||||
* LegendasTV
|
||||
* Ktuvit (Get `hashed_password` using method described [here](https://github.com/XBMCil/service.subtitles.ktuvit))
|
||||
* Napiprojekt
|
||||
* Napisy24
|
||||
* Nekur
|
||||
|
|
|
@ -139,6 +139,10 @@ defaults = {
|
|||
'password': '',
|
||||
'skip_wrong_fps': 'False'
|
||||
},
|
||||
'ktuvit': {
|
||||
'email': '',
|
||||
'hashed_password': ''
|
||||
},
|
||||
'legendastv': {
|
||||
'username': '',
|
||||
'password': '',
|
||||
|
|
|
@ -188,6 +188,10 @@ def get_providers_auth():
|
|||
'username': settings.titlovi.username,
|
||||
'password': settings.titlovi.password,
|
||||
},
|
||||
'ktuvit' : {
|
||||
'email': settings.ktuvit.email,
|
||||
'hashed_password': settings.ktuvit.hashed_password,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -45,8 +45,10 @@ import logging
|
|||
|
||||
def is_virtualenv():
|
||||
# return True if Bazarr have been start from within a virtualenv or venv
|
||||
base_prefix = getattr(sys, "base_prefix", None) or getattr(sys, "real_prefix", None) or sys.prefix
|
||||
return base_prefix != sys.prefix
|
||||
base_prefix = getattr(sys, "base_prefix", None)
|
||||
# real_prefix will return None if not in a virtualenv enviroment or the default python path
|
||||
real_prefix = getattr(sys, "real_prefix", None) or sys.prefix
|
||||
return base_prefix != real_prefix
|
||||
|
||||
|
||||
# deploy requirements.txt
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"react-dom": "^17",
|
||||
"react-helmet": "^6.1",
|
||||
"react-redux": "^7.2",
|
||||
"react-router-dom": "^5.2",
|
||||
"react-router-dom": "^5.3",
|
||||
"react-scripts": "^4",
|
||||
"react-select": "^4",
|
||||
"react-table": "^7",
|
||||
|
@ -17095,11 +17095,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
|
||||
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
|
||||
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
"hoist-non-react-statics": "^3.1.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
|
@ -17115,15 +17115,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
|
||||
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
|
||||
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-router": "5.2.0",
|
||||
"react-router": "5.2.1",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0"
|
||||
},
|
||||
|
@ -18316,9 +18316,9 @@
|
|||
"integrity": "sha512-vTxrZz4dX5W86M6oVWVdOVe72ZiPs41Oi7Z6Km4W5Turyz28mrXSJhhEBZoRtzJWIv3833WKVwLSDWWkEfupMg=="
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.37.5",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.37.5.tgz",
|
||||
"integrity": "sha512-Cx3ewxz9QB/ErnVIiWg2cH0kiYZ0FPvheDTVC6BsiEGBTZKKZJ1Gq5Kq6jy3PKtL6+EJ8NIoaBW/RSd2R6cZOA==",
|
||||
"version": "1.38.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.38.2.tgz",
|
||||
"integrity": "sha512-Bz1fG6qiyF0FX6m/I+VxtdVKz1Dfmg/e9kfDy2PhWOkq3T384q2KxwIfP0fXpeI+EyyETdOauH+cRHQDFASllA==",
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0"
|
||||
},
|
||||
|
@ -35324,11 +35324,11 @@
|
|||
}
|
||||
},
|
||||
"react-router": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.0.tgz",
|
||||
"integrity": "sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw==",
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.2.1.tgz",
|
||||
"integrity": "sha512-lIboRiOtDLFdg1VTemMwud9vRVuOCZmUIT/7lUoZiSpPODiiH1UQlfXy+vPLC/7IWdFYnhRwAyNqA/+I7wnvKQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
"hoist-non-react-statics": "^3.1.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
|
@ -35341,15 +35341,15 @@
|
|||
}
|
||||
},
|
||||
"react-router-dom": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.2.0.tgz",
|
||||
"integrity": "sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA==",
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.0.tgz",
|
||||
"integrity": "sha512-ObVBLjUZsphUUMVycibxgMdh5jJ1e3o+KpAZBVeHcNQZ4W+uUGGWsokurzlF4YOldQYRQL4y6yFRWM4m3svmuQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"@babel/runtime": "^7.12.13",
|
||||
"history": "^4.9.0",
|
||||
"loose-envify": "^1.3.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"react-router": "5.2.0",
|
||||
"react-router": "5.2.1",
|
||||
"tiny-invariant": "^1.0.2",
|
||||
"tiny-warning": "^1.0.0"
|
||||
}
|
||||
|
@ -36292,9 +36292,9 @@
|
|||
"integrity": "sha512-vTxrZz4dX5W86M6oVWVdOVe72ZiPs41Oi7Z6Km4W5Turyz28mrXSJhhEBZoRtzJWIv3833WKVwLSDWWkEfupMg=="
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.37.5",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.37.5.tgz",
|
||||
"integrity": "sha512-Cx3ewxz9QB/ErnVIiWg2cH0kiYZ0FPvheDTVC6BsiEGBTZKKZJ1Gq5Kq6jy3PKtL6+EJ8NIoaBW/RSd2R6cZOA==",
|
||||
"version": "1.38.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.38.2.tgz",
|
||||
"integrity": "sha512-Bz1fG6qiyF0FX6m/I+VxtdVKz1Dfmg/e9kfDy2PhWOkq3T384q2KxwIfP0fXpeI+EyyETdOauH+cRHQDFASllA==",
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0"
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"react-dom": "^17",
|
||||
"react-helmet": "^6.1",
|
||||
"react-redux": "^7.2",
|
||||
"react-router-dom": "^5.2",
|
||||
"react-router-dom": "^5.3",
|
||||
"react-scripts": "^4",
|
||||
"react-select": "^4",
|
||||
"react-table": "^7",
|
||||
|
|
|
@ -47,7 +47,9 @@ export const siteUpdateNotifier = createAction<string>(
|
|||
"site/progress/update_notifier"
|
||||
);
|
||||
|
||||
export const siteChangeSidebar = createAction<string>("site/sidebar/update");
|
||||
export const siteChangeSidebarVisibility = createAction<boolean>(
|
||||
"site/sidebar/visibility"
|
||||
);
|
||||
|
||||
export const siteUpdateOffline = createAction<boolean>("site/offline/update");
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useCallback, useEffect } from "react";
|
||||
import { useCallback } from "react";
|
||||
import { useSystemSettings } from ".";
|
||||
import { siteAddNotifications, siteChangeSidebar } from "../actions";
|
||||
import { siteAddNotifications } from "../actions";
|
||||
import { useReduxAction, useReduxStore } from "./base";
|
||||
|
||||
export function useNotification(id: string, timeout: number = 5000) {
|
||||
|
@ -37,10 +37,3 @@ export function useShowOnlyDesired() {
|
|||
const settings = useSystemSettings();
|
||||
return settings.content?.general.embedded_subs_show_desired ?? false;
|
||||
}
|
||||
|
||||
export function useSetSidebar(key: string) {
|
||||
const update = useReduxAction(siteChangeSidebar);
|
||||
useEffect(() => {
|
||||
update(key);
|
||||
}, [update, key]);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
siteAddNotifications,
|
||||
siteAddProgress,
|
||||
siteBootstrap,
|
||||
siteChangeSidebar,
|
||||
siteChangeSidebarVisibility,
|
||||
siteRedirectToAuth,
|
||||
siteRemoveNotifications,
|
||||
siteRemoveProgress,
|
||||
|
@ -28,7 +28,7 @@ interface Site {
|
|||
timestamp: string;
|
||||
};
|
||||
notifications: Server.Notification[];
|
||||
sidebar: string;
|
||||
showSidebar: boolean;
|
||||
badges: Badge;
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ const defaultSite: Site = {
|
|||
timestamp: String(Date.now()),
|
||||
},
|
||||
notifications: [],
|
||||
sidebar: "",
|
||||
showSidebar: false,
|
||||
badges: {
|
||||
movies: 0,
|
||||
episodes: 0,
|
||||
|
@ -116,8 +116,8 @@ const reducer = createReducer(defaultSite, (builder) => {
|
|||
});
|
||||
|
||||
builder
|
||||
.addCase(siteChangeSidebar, (state, action) => {
|
||||
state.sidebar = action.payload;
|
||||
.addCase(siteChangeSidebarVisibility, (state, action) => {
|
||||
state.showSidebar = action.payload;
|
||||
})
|
||||
.addCase(siteUpdateOffline, (state, action) => {
|
||||
state.offline = action.payload;
|
||||
|
|
|
@ -21,6 +21,7 @@ interface Settings {
|
|||
subscene: Settings.Subscene;
|
||||
betaseries: Settings.Betaseries;
|
||||
titlovi: Settings.Titlovi;
|
||||
ktuvit: Settings.Ktuvit;
|
||||
notifications: Settings.Notifications;
|
||||
}
|
||||
|
||||
|
@ -193,6 +194,11 @@ declare namespace Settings {
|
|||
|
||||
interface Titlovi extends BaseProvider {}
|
||||
|
||||
interface Ktuvit {
|
||||
email?: string;
|
||||
hashed_password?: string;
|
||||
}
|
||||
|
||||
interface Betaseries {
|
||||
token?: string;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
faUser,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { FunctionComponent, useContext, useMemo } from "react";
|
||||
import React, { FunctionComponent, useMemo } from "react";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
|
@ -16,8 +16,10 @@ import {
|
|||
Row,
|
||||
} from "react-bootstrap";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { SidebarToggleContext } from ".";
|
||||
import { siteRedirectToAuth } from "../@redux/actions";
|
||||
import {
|
||||
siteChangeSidebarVisibility,
|
||||
siteRedirectToAuth,
|
||||
} from "../@redux/actions";
|
||||
import { useSystemSettings } from "../@redux/hooks";
|
||||
import { useReduxAction } from "../@redux/hooks/base";
|
||||
import { useIsOffline } from "../@redux/hooks/site";
|
||||
|
@ -56,7 +58,7 @@ const Header: FunctionComponent<Props> = () => {
|
|||
|
||||
const canLogout = (settings.content?.auth.type ?? "none") === "form";
|
||||
|
||||
const toggleSidebar = useContext(SidebarToggleContext);
|
||||
const changeSidebar = useReduxAction(siteChangeSidebarVisibility);
|
||||
|
||||
const offline = useIsOffline();
|
||||
|
||||
|
@ -115,7 +117,10 @@ const Header: FunctionComponent<Props> = () => {
|
|||
className="cursor-pointer"
|
||||
></Image>
|
||||
</div>
|
||||
<Button className="mx-2 m-0 d-md-none" onClick={toggleSidebar}>
|
||||
<Button
|
||||
className="mx-2 m-0 d-md-none"
|
||||
onClick={() => changeSidebar(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faBars}></FontAwesomeIcon>
|
||||
</Button>
|
||||
<Container fluid>
|
||||
|
|
|
@ -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,9 +1,4 @@
|
|||
import React, {
|
||||
FunctionComponent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { FunctionComponent, useEffect } from "react";
|
||||
import { Row } from "react-bootstrap";
|
||||
import { Provider } from "react-redux";
|
||||
import { Route, Switch } from "react-router";
|
||||
|
@ -14,16 +9,15 @@ import { useReduxStore } from "../@redux/hooks/base";
|
|||
import { useNotification } from "../@redux/hooks/site";
|
||||
import store from "../@redux/store";
|
||||
import { LoadingIndicator, ModalProvider } from "../components";
|
||||
import Router from "../Router";
|
||||
import Sidebar from "../Sidebar";
|
||||
import Auth from "../special-pages/AuthPage";
|
||||
import ErrorBoundary from "../special-pages/ErrorBoundary";
|
||||
import LaunchError from "../special-pages/LaunchError";
|
||||
import { Environment } from "../utilities";
|
||||
import Header from "./Header";
|
||||
import Router from "./Router";
|
||||
|
||||
// Sidebar Toggle
|
||||
export const SidebarToggleContext = React.createContext<() => void>(() => {});
|
||||
|
||||
interface Props {}
|
||||
|
||||
|
@ -43,9 +37,6 @@ const App: FunctionComponent<Props> = () => {
|
|||
}
|
||||
}, initialized === true);
|
||||
|
||||
const [sidebar, setSidebar] = useState(false);
|
||||
const toggleSidebar = useCallback(() => setSidebar((s) => !s), []);
|
||||
|
||||
if (!auth) {
|
||||
return <Redirect to="/login"></Redirect>;
|
||||
}
|
||||
|
@ -61,17 +52,15 @@ const App: FunctionComponent<Props> = () => {
|
|||
}
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<SidebarToggleContext.Provider value={toggleSidebar}>
|
||||
<Row noGutters className="header-container">
|
||||
<Header></Header>
|
||||
</Row>
|
||||
<Row noGutters className="flex-nowrap">
|
||||
<Sidebar open={sidebar}></Sidebar>
|
||||
<ModalProvider>
|
||||
<Router className="d-flex flex-row flex-grow-1 main-router"></Router>
|
||||
</ModalProvider>
|
||||
</Row>
|
||||
</SidebarToggleContext.Provider>
|
||||
<Row noGutters className="header-container">
|
||||
<Header></Header>
|
||||
</Row>
|
||||
<Row noGutters className="flex-nowrap">
|
||||
<Sidebar></Sidebar>
|
||||
<ModalProvider>
|
||||
<Router></Router>
|
||||
</ModalProvider>
|
||||
</Row>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -102,7 +102,7 @@ const SeriesEpisodesView: FunctionComponent<Props> = (props) => {
|
|||
seriesid: id,
|
||||
}
|
||||
);
|
||||
dispatchTask("Scaning disk...", [task], "Scaning...");
|
||||
dispatchTask("Scanning disk...", [task], "Scanning...");
|
||||
}}
|
||||
>
|
||||
Scan Disk
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -70,6 +70,18 @@ export const ProviderList: Readonly<ProviderInfo[]> = [
|
|||
skip_wrong_fps: "Skip Wrong FPS",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "ktuvit",
|
||||
name: "Ktuvit",
|
||||
description: "Hebrew Subtitles Provider",
|
||||
defaultKey: {
|
||||
email: "",
|
||||
hashed_password: ""
|
||||
},
|
||||
keyNameOverride: {
|
||||
hashed_password: "Hashed Password",
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "legendastv",
|
||||
name: "LegendasTV",
|
||||
|
|
|
@ -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,86 +1,56 @@
|
|||
import React, { FunctionComponent, useContext, useMemo } from "react";
|
||||
import { Container, Image, ListGroup } from "react-bootstrap";
|
||||
import { useReduxStore } from "../@redux/hooks/base";
|
||||
import { useIsRadarrEnabled, useIsSonarrEnabled } from "../@redux/hooks/site";
|
||||
import logo from "../@static/logo64.png";
|
||||
import { SidebarToggleContext } from "../App";
|
||||
import { useGotoHomepage } from "../utilities/hooks";
|
||||
import { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, {
|
||||
createContext,
|
||||
FunctionComponent,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
BadgesContext,
|
||||
CollapseItem,
|
||||
HiddenKeysContext,
|
||||
LinkItem,
|
||||
} from "./items";
|
||||
import { RadarrDisabledKey, SidebarList, SonarrDisabledKey } from "./list";
|
||||
Badge,
|
||||
Collapse,
|
||||
Container,
|
||||
Image,
|
||||
ListGroup,
|
||||
ListGroupItem,
|
||||
} from "react-bootstrap";
|
||||
import { NavLink, useHistory, useRouteMatch } from "react-router-dom";
|
||||
import { siteChangeSidebarVisibility } from "../@redux/actions";
|
||||
import { useReduxAction, useReduxStore } from "../@redux/hooks/base";
|
||||
import logo from "../@static/logo64.png";
|
||||
import { useNavigationItems } from "../Navigation";
|
||||
import { Navigation } from "../Navigation/nav";
|
||||
import { BuildKey } from "../utilities";
|
||||
import { useGotoHomepage } from "../utilities/hooks";
|
||||
import "./style.scss";
|
||||
import { BadgeProvider } from "./types";
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
}
|
||||
const SelectionContext = createContext<{
|
||||
selection: string | null;
|
||||
select: (selection: string | null) => void;
|
||||
}>({ selection: null, select: () => {} });
|
||||
|
||||
const Sidebar: FunctionComponent<Props> = ({ open }) => {
|
||||
const toggle = useContext(SidebarToggleContext);
|
||||
const Sidebar: FunctionComponent = () => {
|
||||
const open = useReduxStore((s) => s.site.showSidebar);
|
||||
|
||||
const { movies, episodes, providers, status } = useReduxStore(
|
||||
(s) => s.site.badges
|
||||
);
|
||||
|
||||
const sonarrEnabled = useIsSonarrEnabled();
|
||||
const radarrEnabled = useIsRadarrEnabled();
|
||||
|
||||
const badges = useMemo<BadgeProvider>(
|
||||
() => ({
|
||||
Wanted: {
|
||||
Series: sonarrEnabled ? episodes : 0,
|
||||
Movies: radarrEnabled ? movies : 0,
|
||||
},
|
||||
System: {
|
||||
Providers: providers,
|
||||
Status: status,
|
||||
},
|
||||
}),
|
||||
[movies, episodes, providers, sonarrEnabled, radarrEnabled, status]
|
||||
);
|
||||
|
||||
const hiddenKeys = useMemo<string[]>(() => {
|
||||
const list = [];
|
||||
if (!sonarrEnabled) {
|
||||
list.push(SonarrDisabledKey);
|
||||
}
|
||||
if (!radarrEnabled) {
|
||||
list.push(RadarrDisabledKey);
|
||||
}
|
||||
return list;
|
||||
}, [sonarrEnabled, radarrEnabled]);
|
||||
const changeSidebar = useReduxAction(siteChangeSidebarVisibility);
|
||||
|
||||
const cls = ["sidebar-container"];
|
||||
const overlay = ["sidebar-overlay"];
|
||||
|
||||
if (open === true) {
|
||||
if (open) {
|
||||
cls.push("open");
|
||||
overlay.push("open");
|
||||
}
|
||||
|
||||
const sidebarItems = useMemo(
|
||||
() =>
|
||||
SidebarList.map((v) => {
|
||||
if (hiddenKeys.includes(v.hiddenKey ?? "")) {
|
||||
return null;
|
||||
}
|
||||
if ("children" in v) {
|
||||
return <CollapseItem key={v.name} {...v}></CollapseItem>;
|
||||
} else {
|
||||
return <LinkItem key={v.link} {...v}></LinkItem>;
|
||||
}
|
||||
}),
|
||||
[hiddenKeys]
|
||||
);
|
||||
|
||||
const goHome = useGotoHomepage();
|
||||
|
||||
const [selection, setSelection] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<SelectionContext.Provider
|
||||
value={{ selection: selection, select: setSelection }}
|
||||
>
|
||||
<aside className={cls.join(" ")}>
|
||||
<Container className="sidebar-title d-flex align-items-center d-md-none">
|
||||
<Image
|
||||
|
@ -92,13 +62,184 @@ const Sidebar: FunctionComponent<Props> = ({ open }) => {
|
|||
className="cursor-pointer"
|
||||
></Image>
|
||||
</Container>
|
||||
<HiddenKeysContext.Provider value={hiddenKeys}>
|
||||
<BadgesContext.Provider value={badges}>
|
||||
<ListGroup variant="flush">{sidebarItems}</ListGroup>
|
||||
</BadgesContext.Provider>
|
||||
</HiddenKeysContext.Provider>
|
||||
<SidebarNavigation></SidebarNavigation>
|
||||
</aside>
|
||||
<div className={overlay.join(" ")} onClick={toggle}></div>
|
||||
<div
|
||||
className={overlay.join(" ")}
|
||||
onClick={() => changeSidebar(false)}
|
||||
></div>
|
||||
</SelectionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarNavigation: FunctionComponent = () => {
|
||||
const navItems = useNavigationItems();
|
||||
|
||||
return (
|
||||
<ListGroup variant="flush">
|
||||
{navItems.map((v, idx) => {
|
||||
if ("routes" in v) {
|
||||
return (
|
||||
<SidebarParent key={BuildKey(idx, v.name)} {...v}></SidebarParent>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<SidebarChild
|
||||
parent=""
|
||||
key={BuildKey(idx, v.name)}
|
||||
{...v}
|
||||
></SidebarChild>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ListGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarParent: FunctionComponent<Navigation.RouteWithChild> = ({
|
||||
icon,
|
||||
badge,
|
||||
name,
|
||||
path,
|
||||
routes,
|
||||
enabled,
|
||||
component,
|
||||
}) => {
|
||||
const computedBadge = useMemo(() => {
|
||||
let computed = badge ?? 0;
|
||||
|
||||
computed += routes.reduce((prev, curr) => {
|
||||
return prev + (curr.badge ?? 0);
|
||||
}, 0);
|
||||
|
||||
return computed !== 0 ? computed : undefined;
|
||||
}, [badge, routes]);
|
||||
|
||||
const enabledRoutes = useMemo(
|
||||
() => routes.filter((v) => v.enabled !== false && v.routeOnly !== true),
|
||||
[routes]
|
||||
);
|
||||
|
||||
const changeSidebar = useReduxAction(siteChangeSidebarVisibility);
|
||||
|
||||
const { selection, select } = useContext(SelectionContext);
|
||||
|
||||
const match = useRouteMatch({ path });
|
||||
const open = match !== null || selection === path;
|
||||
|
||||
const collapseBoxClass = useMemo(
|
||||
() => `sidebar-collapse-box ${open ? "active" : ""}`,
|
||||
[open]
|
||||
);
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
if (enabled === false) {
|
||||
return null;
|
||||
} else if (enabledRoutes.length === 0) {
|
||||
if (component) {
|
||||
return (
|
||||
<NavLink
|
||||
activeClassName="sb-active"
|
||||
className="list-group-item list-group-item-action sidebar-button"
|
||||
to={path}
|
||||
onClick={() => changeSidebar(false)}
|
||||
>
|
||||
<SidebarContent
|
||||
icon={icon}
|
||||
name={name}
|
||||
badge={computedBadge}
|
||||
></SidebarContent>
|
||||
</NavLink>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={collapseBoxClass}>
|
||||
<ListGroupItem
|
||||
action
|
||||
className="sidebar-button"
|
||||
onClick={() => {
|
||||
if (open) {
|
||||
select(null);
|
||||
} else {
|
||||
select(path);
|
||||
}
|
||||
if (component !== undefined) {
|
||||
history.push(path);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SidebarContent
|
||||
icon={icon}
|
||||
name={name}
|
||||
badge={computedBadge}
|
||||
></SidebarContent>
|
||||
</ListGroupItem>
|
||||
<Collapse in={open}>
|
||||
<div className="sidebar-collapse">
|
||||
{enabledRoutes.map((v, idx) => (
|
||||
<SidebarChild
|
||||
key={BuildKey(idx, v.name, "child")}
|
||||
parent={path}
|
||||
{...v}
|
||||
></SidebarChild>
|
||||
))}
|
||||
</div>
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface SidebarChildProps {
|
||||
parent: string;
|
||||
}
|
||||
|
||||
const SidebarChild: FunctionComponent<
|
||||
SidebarChildProps & Navigation.RouteWithoutChild
|
||||
> = ({ icon, name, path, badge, enabled, routeOnly, parent }) => {
|
||||
const changeSidebar = useReduxAction(siteChangeSidebarVisibility);
|
||||
const { select } = useContext(SelectionContext);
|
||||
|
||||
if (enabled === false || routeOnly === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
activeClassName="sb-active"
|
||||
className="list-group-item list-group-item-action sidebar-button sb-collapse"
|
||||
to={parent + path}
|
||||
onClick={() => {
|
||||
select(null);
|
||||
changeSidebar(false);
|
||||
}}
|
||||
>
|
||||
<SidebarContent icon={icon} name={name} badge={badge}></SidebarContent>
|
||||
</NavLink>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarContent: FunctionComponent<{
|
||||
icon?: IconDefinition;
|
||||
name: string;
|
||||
badge?: number;
|
||||
}> = ({ icon, name, badge }) => {
|
||||
return (
|
||||
<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 !== 0 ? badge : null}</Badge>
|
||||
</span>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -7,7 +7,7 @@ import { BuildKey } from "../../utilities";
|
|||
|
||||
interface Props {}
|
||||
|
||||
const ReleasesView: FunctionComponent<Props> = () => {
|
||||
const SystemReleasesView: FunctionComponent<Props> = () => {
|
||||
const releases = useSystemReleases();
|
||||
|
||||
return (
|
||||
|
@ -32,25 +32,6 @@ const ReleasesView: FunctionComponent<Props> = () => {
|
|||
</Row>
|
||||
</Container>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <AsyncStateOverlay state={releases}>
|
||||
// {({ data }) => (
|
||||
// <Container fluid className="px-5 py-4 bg-light">
|
||||
// <Helmet>
|
||||
// <title>Releases - Bazarr (System)</title>
|
||||
// </Helmet>
|
||||
// <Row>
|
||||
// {data.map((v, idx) => (
|
||||
// <Col xs={12} key={BuildKey(idx, v.date)}>
|
||||
// <InfoElement {...v}></InfoElement>
|
||||
// </Col>
|
||||
// ))}
|
||||
// </Row>
|
||||
// </Container>
|
||||
// )}
|
||||
// </AsyncStateOverlay>
|
||||
// );
|
||||
};
|
||||
|
||||
const headerBadgeCls = "mr-2";
|
||||
|
@ -95,4 +76,4 @@ const InfoElement: FunctionComponent<ReleaseInfo> = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default ReleasesView;
|
||||
export default SystemReleasesView;
|
||||
|
|
|
@ -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;
|
|
@ -6,7 +6,6 @@ import React, {
|
|||
MouseEvent,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
|
@ -59,16 +58,13 @@ export function ContentHeaderAsyncButton<T extends () => Promise<any>>(
|
|||
|
||||
const [updating, setUpdate] = useState(false);
|
||||
|
||||
const promiseRef = useRef(promise);
|
||||
const successRef = useRef(onSuccess);
|
||||
|
||||
const click = useCallback(() => {
|
||||
setUpdate(true);
|
||||
promiseRef.current().then((val) => {
|
||||
promise().then((val) => {
|
||||
setUpdate(false);
|
||||
successRef.current && successRef.current(val);
|
||||
onSuccess && onSuccess(val);
|
||||
});
|
||||
}, [successRef, promiseRef]);
|
||||
}, [onSuccess, promise]);
|
||||
|
||||
return (
|
||||
<ContentHeaderButton
|
||||
|
|
|
@ -26,17 +26,15 @@ export const Chips: FunctionComponent<ChipsProps> = ({
|
|||
|
||||
const input = useRef<HTMLInputElement>(null);
|
||||
|
||||
const changeRef = useRef(onChange);
|
||||
|
||||
const addChip = useCallback(
|
||||
(value: string) => {
|
||||
setChips((cp) => {
|
||||
const newChips = [...cp, value];
|
||||
changeRef.current && changeRef.current(newChips);
|
||||
onChange && onChange(newChips);
|
||||
return newChips;
|
||||
});
|
||||
},
|
||||
[changeRef]
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const removeChip = useCallback(
|
||||
|
@ -46,14 +44,14 @@ export const Chips: FunctionComponent<ChipsProps> = ({
|
|||
if (index !== -1) {
|
||||
const newChips = [...cp];
|
||||
newChips.splice(index, 1);
|
||||
changeRef.current && changeRef.current(newChips);
|
||||
onChange && onChange(newChips);
|
||||
return newChips;
|
||||
} else {
|
||||
return cp;
|
||||
}
|
||||
});
|
||||
},
|
||||
[changeRef]
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const clearInput = useCallback(() => {
|
||||
|
|
|
@ -49,22 +49,27 @@ class Addic7edSubtitle(Subtitle):
|
|||
def get_matches(self, video):
|
||||
matches = set()
|
||||
|
||||
# series name
|
||||
if video.series and sanitize(self.series) 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')
|
||||
if isinstance(video, Episode):
|
||||
# series name
|
||||
if video.series and sanitize(self.series) 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')
|
||||
# year
|
||||
if video.original_series and self.year is None or video.year and video.year == self.year:
|
||||
matches.add('year')
|
||||
else:
|
||||
# year
|
||||
if video.year and video.year == self.year:
|
||||
matches.add('year')
|
||||
# title of the episode
|
||||
if video.title and sanitize(self.title) == sanitize(video.title):
|
||||
matches.add('title')
|
||||
# year
|
||||
if video.original_series and self.year is None or video.year and video.year == self.year:
|
||||
matches.add('year')
|
||||
# release_group
|
||||
if (video.release_group and self.version and
|
||||
any(r in sanitize_release_group(self.version)
|
||||
|
|
|
@ -6,10 +6,12 @@ import subliminal
|
|||
import time
|
||||
|
||||
from random import randint
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from dogpile.cache.api import NO_VALUE
|
||||
from requests import Session
|
||||
from subliminal.cache import region
|
||||
from subliminal.video import Episode, Movie
|
||||
from subliminal.exceptions import DownloadLimitExceeded, AuthenticationError, ConfigurationError
|
||||
from subliminal.providers.addic7ed import Addic7edProvider as _Addic7edProvider, \
|
||||
Addic7edSubtitle as _Addic7edSubtitle, ParserBeautifulSoup
|
||||
|
@ -25,16 +27,18 @@ logger = logging.getLogger(__name__)
|
|||
series_year_re = re.compile(r'^(?P<series>[ \w\'.:(),*&!?-]+?)(?: \((?P<year>\d{4})\))?$')
|
||||
|
||||
SHOW_EXPIRATION_TIME = datetime.timedelta(weeks=1).total_seconds()
|
||||
MOVIE_EXPIRATION_TIME = datetime.timedelta(weeks=1).total_seconds()
|
||||
|
||||
|
||||
class Addic7edSubtitle(_Addic7edSubtitle):
|
||||
hearing_impaired_verifiable = True
|
||||
|
||||
def __init__(self, language, hearing_impaired, page_link, series, season, episode, title, year, version,
|
||||
download_link):
|
||||
download_link, uploader=None):
|
||||
super(Addic7edSubtitle, self).__init__(language, hearing_impaired, page_link, series, season, episode,
|
||||
title, year, version, download_link)
|
||||
self.release_info = version.replace('+', ',')
|
||||
self.uploader = uploader
|
||||
|
||||
def get_matches(self, video):
|
||||
matches = super(Addic7edSubtitle, self).get_matches(video)
|
||||
|
@ -63,6 +67,7 @@ class Addic7edProvider(_Addic7edProvider):
|
|||
]} | {Language.fromietf(l) for l in ["sr-Latn", "sr-Cyrl"]}
|
||||
languages.update(set(Language.rebuild(l, hi=True) for l in languages))
|
||||
|
||||
video_types = (Episode, Movie)
|
||||
USE_ADDICTED_RANDOM_AGENTS = False
|
||||
hearing_impaired_verifiable = True
|
||||
subtitle_class = Addic7edSubtitle
|
||||
|
@ -218,6 +223,52 @@ class Addic7edProvider(_Addic7edProvider):
|
|||
|
||||
return show_id
|
||||
|
||||
@region.cache_on_arguments(expiration_time=MOVIE_EXPIRATION_TIME)
|
||||
def get_movie_id(self, movie, year=None):
|
||||
"""Get the best matching movie id for `movie`, `year`.
|
||||
|
||||
:param str movie: movie.
|
||||
:param year: year of the movie, if any.
|
||||
:type year: int
|
||||
:return: the movie id, if found.
|
||||
:rtype: int
|
||||
"""
|
||||
movie_id = None
|
||||
|
||||
# get the movie id
|
||||
logger.info('Getting movie id')
|
||||
|
||||
r = self.session.get(self.server_url + 'search.php?search=' + quote_plus(movie), timeout=60)
|
||||
r.raise_for_status()
|
||||
|
||||
soup = ParserBeautifulSoup(r.content.decode('utf-8', 'ignore'), ['lxml', 'html.parser'])
|
||||
|
||||
# populate the movie id
|
||||
movies_table = soup.find('table', {'class': 'tabel'})
|
||||
movies = movies_table.find_all('tr')
|
||||
for item in movies:
|
||||
link = item.find('a', href=True)
|
||||
if link:
|
||||
type, media_id = link['href'].split('/')
|
||||
if type == 'movie':
|
||||
media_title = link.text
|
||||
match = re.search(r'(.+)\s\((\d{4})\)$', media_title)
|
||||
if match:
|
||||
media_name = match.group(1)
|
||||
media_year = match.group(2)
|
||||
if sanitize(media_name.lower()) == sanitize(movie.lower()) and media_year == str(year):
|
||||
movie_id = media_id
|
||||
|
||||
soup.decompose()
|
||||
soup = None
|
||||
|
||||
logger.debug(f'Found this movie id: {movie_id}')
|
||||
|
||||
if not movie_id:
|
||||
logging.debug(f"Addic7ed: Cannot find this movie with guessed year {year}: {movie}")
|
||||
|
||||
return movie_id
|
||||
|
||||
@region.cache_on_arguments(expiration_time=SHOW_EXPIRATION_TIME)
|
||||
def _get_show_ids(self):
|
||||
"""Get the ``dict`` of show ids per series by querying the `shows.php` page.
|
||||
|
@ -390,6 +441,110 @@ class Addic7edProvider(_Addic7edProvider):
|
|||
|
||||
return subtitles
|
||||
|
||||
def query_movie(self, movie_id, title, year=None):
|
||||
# get the page of the movie
|
||||
logger.info('Getting the page of movie id %d', movie_id)
|
||||
r = self.session.get(self.server_url + 'movie/' + movie_id,
|
||||
timeout=60,
|
||||
headers={
|
||||
"referer": self.server_url,
|
||||
"X-Requested-With": "XMLHttpRequest"
|
||||
}
|
||||
)
|
||||
|
||||
r.raise_for_status()
|
||||
|
||||
if r.status_code == 304:
|
||||
raise TooManyRequests()
|
||||
|
||||
if not r.text:
|
||||
# Provider wrongful return a status of 304 Not Modified with an empty content
|
||||
# raise_for_status won't raise exception for that status code
|
||||
logger.error('No data returned from provider')
|
||||
return []
|
||||
|
||||
soup = ParserBeautifulSoup(r.content, ['lxml', 'html.parser'])
|
||||
|
||||
# loop over subtitle rows
|
||||
tables = []
|
||||
subtitles = []
|
||||
for table in soup.find_all('table', {'align': 'center',
|
||||
'border': '0',
|
||||
'class': 'tabel95',
|
||||
'width': '100%'}):
|
||||
if table.find_all('td', {'class': 'NewsTitle'}):
|
||||
tables.append(table)
|
||||
for table in tables:
|
||||
row1 = table.contents[1]
|
||||
row2 = table.contents[4]
|
||||
row3 = table.contents[6]
|
||||
# other rows are useless
|
||||
|
||||
# ignore incomplete subtitles
|
||||
status = row2.contents[6].text
|
||||
if "%" in status:
|
||||
logger.debug('Ignoring subtitle with status %s', status)
|
||||
continue
|
||||
|
||||
# read the item
|
||||
language = Language.fromaddic7ed(row2.contents[4].text.strip('\n'))
|
||||
hearing_impaired = bool(row3.contents[1].contents[1].attrs['src'].endswith('hi.jpg'))
|
||||
page_link = self.server_url + 'movie/' + movie_id
|
||||
version_matches = re.search(r'Version\s(.+),.+', str(row1.contents[1].contents[1]))
|
||||
version = version_matches.group(1) if version_matches else None
|
||||
download_link = row2.contents[8].contents[2].attrs['href'][1:]
|
||||
uploader = row1.contents[2].contents[8].text.strip()
|
||||
|
||||
# set subtitle language to hi if it's hearing_impaired
|
||||
if hearing_impaired:
|
||||
language = Language.rebuild(language, hi=True)
|
||||
|
||||
subtitle = self.subtitle_class(language, hearing_impaired, page_link, None, None, None, title, year,
|
||||
version, download_link, uploader)
|
||||
logger.debug('Found subtitle %r', subtitle)
|
||||
subtitles.append(subtitle)
|
||||
|
||||
soup.decompose()
|
||||
soup = None
|
||||
|
||||
return subtitles
|
||||
|
||||
def list_subtitles(self, video, languages):
|
||||
if isinstance(video, Episode):
|
||||
# lookup show_id
|
||||
titles = [video.series] + video.alternative_series[5:]
|
||||
show_id = None
|
||||
for title in titles:
|
||||
show_id = self.get_show_id(title, video.year)
|
||||
if show_id is not None:
|
||||
break
|
||||
|
||||
# query for subtitles with the show_id
|
||||
if show_id is not None:
|
||||
subtitles = [s for s in self.query(show_id, title, video.season, video.year)
|
||||
if s.language in languages and s.episode == video.episode]
|
||||
if subtitles:
|
||||
return subtitles
|
||||
else:
|
||||
logger.error('No show id found for %r (%r)', video.series, {'year': video.year})
|
||||
else:
|
||||
titles = [video.title] + video.alternative_titles[5:]
|
||||
|
||||
for title in titles:
|
||||
movie_id = self.get_movie_id(title, video.year)
|
||||
if movie_id is not None:
|
||||
break
|
||||
|
||||
# query for subtitles with the movie_id
|
||||
if movie_id is not None:
|
||||
subtitles = [s for s in self.query_movie(movie_id, title, video.year) if s.language in languages]
|
||||
if subtitles:
|
||||
return subtitles
|
||||
else:
|
||||
logger.error('No movie id found for %r (%r)', video.title, {'year': video.year})
|
||||
|
||||
return []
|
||||
|
||||
def download_subtitle(self, subtitle):
|
||||
# download the subtitle
|
||||
r = self.session.get(self.server_url + subtitle.download_link, headers={'Referer': subtitle.page_link},
|
||||
|
|
|
@ -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)
|
|
@ -97,7 +97,7 @@ class TitrariProvider(Provider, ProviderSubtitleArchiveMixin):
|
|||
languages = {Language(l) for l in ['ron', 'eng']}
|
||||
languages.update(set(Language.rebuild(l, forced=True) for l in languages))
|
||||
api_url = 'https://www.titrari.ro/'
|
||||
query_advanced_search = 'cautaredevansata'
|
||||
query_advanced_search = 'cautarenedevansata'
|
||||
|
||||
def __init__(self):
|
||||
self.session = None
|
||||
|
|
Loading…
Reference in New Issue