From 3c790549e0278d6e9263b180d491b2414f6c3734 Mon Sep 17 00:00:00 2001 From: LASER-Yi Date: Wed, 1 Sep 2021 00:08:47 +0800 Subject: [PATCH 01/13] Rewrite router and sidebar --- frontend/src/@redux/actions/site.ts | 4 +- frontend/src/@redux/hooks/site.ts | 11 +- frontend/src/@redux/reducers/site.ts | 10 +- frontend/src/App/Header.tsx | 15 +- frontend/src/App/Router.tsx | 71 ------ frontend/src/App/index.tsx | 33 +-- frontend/src/Blacklist/Router.tsx | 36 ---- frontend/src/DisplayItem/Router.tsx | 45 ---- frontend/src/History/Router.tsx | 40 ---- frontend/src/Navigation/index.ts | 238 ++++++++++++++++++++ frontend/src/Navigation/nav.d.ts | 26 +++ frontend/src/Router/index.tsx | 83 +++++++ frontend/src/Settings/Router.tsx | 58 ----- frontend/src/Sidebar/index.tsx | 287 ++++++++++++++++++------- frontend/src/Sidebar/items.tsx | 179 --------------- frontend/src/Sidebar/list.ts | 148 ------------- frontend/src/Sidebar/types.d.ts | 29 --- frontend/src/System/Releases/index.tsx | 23 +- frontend/src/System/Router.tsx | 37 ---- frontend/src/Wanted/Router.tsx | 36 ---- 20 files changed, 594 insertions(+), 815 deletions(-) delete mode 100644 frontend/src/App/Router.tsx delete mode 100644 frontend/src/Blacklist/Router.tsx delete mode 100644 frontend/src/DisplayItem/Router.tsx delete mode 100644 frontend/src/History/Router.tsx create mode 100644 frontend/src/Navigation/index.ts create mode 100644 frontend/src/Navigation/nav.d.ts create mode 100644 frontend/src/Router/index.tsx delete mode 100644 frontend/src/Settings/Router.tsx delete mode 100644 frontend/src/Sidebar/items.tsx delete mode 100644 frontend/src/Sidebar/list.ts delete mode 100644 frontend/src/Sidebar/types.d.ts delete mode 100644 frontend/src/System/Router.tsx delete mode 100644 frontend/src/Wanted/Router.tsx diff --git a/frontend/src/@redux/actions/site.ts b/frontend/src/@redux/actions/site.ts index b151bfb5f..fc348b942 100644 --- a/frontend/src/@redux/actions/site.ts +++ b/frontend/src/@redux/actions/site.ts @@ -47,7 +47,9 @@ export const siteUpdateNotifier = createAction( "site/progress/update_notifier" ); -export const siteChangeSidebar = createAction("site/sidebar/update"); +export const siteChangeSidebarVisibility = createAction( + "site/sidebar/visibility" +); export const siteUpdateOffline = createAction("site/offline/update"); diff --git a/frontend/src/@redux/hooks/site.ts b/frontend/src/@redux/hooks/site.ts index 05354572b..8d93fc13f 100644 --- a/frontend/src/@redux/hooks/site.ts +++ b/frontend/src/@redux/hooks/site.ts @@ -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]); -} diff --git a/frontend/src/@redux/reducers/site.ts b/frontend/src/@redux/reducers/site.ts index 8381a95b4..21cc0b370 100644 --- a/frontend/src/@redux/reducers/site.ts +++ b/frontend/src/@redux/reducers/site.ts @@ -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; diff --git a/frontend/src/App/Header.tsx b/frontend/src/App/Header.tsx index ae413030c..9ca33a574 100644 --- a/frontend/src/App/Header.tsx +++ b/frontend/src/App/Header.tsx @@ -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 = () => { 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 = () => { className="cursor-pointer" > - diff --git a/frontend/src/App/Router.tsx b/frontend/src/App/Router.tsx deleted file mode 100644 index 626ec6a4d..000000000 --- a/frontend/src/App/Router.tsx +++ /dev/null @@ -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 ( -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- ); -}; - -export default Router; diff --git a/frontend/src/App/index.tsx b/frontend/src/App/index.tsx index 9c16ee800..d88651e7b 100644 --- a/frontend/src/App/index.tsx +++ b/frontend/src/App/index.tsx @@ -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 = () => { } }, initialized === true); - const [sidebar, setSidebar] = useState(false); - const toggleSidebar = useCallback(() => setSidebar((s) => !s), []); - if (!auth) { return ; } @@ -61,17 +52,15 @@ const App: FunctionComponent = () => { } return ( - - -
-
- - - - - - -
+ +
+
+ + + + + +
); }; diff --git a/frontend/src/Blacklist/Router.tsx b/frontend/src/Blacklist/Router.tsx deleted file mode 100644 index 886a11514..000000000 --- a/frontend/src/Blacklist/Router.tsx +++ /dev/null @@ -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 ( - - {sonarr && ( - - - - )} - {radarr && ( - - - - )} - - - - - ); -}; - -export default Router; diff --git a/frontend/src/DisplayItem/Router.tsx b/frontend/src/DisplayItem/Router.tsx deleted file mode 100644 index 1ab2c4f8a..000000000 --- a/frontend/src/DisplayItem/Router.tsx +++ /dev/null @@ -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 = () => { - const radarr = useIsRadarrEnabled(); - const sonarr = useIsSonarrEnabled(); - - return ( - - {radarr && ( - - - - )} - {radarr && ( - - - - )} - {sonarr && ( - - - - )} - {sonarr && ( - - - - )} - - - - - ); -}; - -export default Router; diff --git a/frontend/src/History/Router.tsx b/frontend/src/History/Router.tsx deleted file mode 100644 index b7693355f..000000000 --- a/frontend/src/History/Router.tsx +++ /dev/null @@ -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 ( - - {sonarr && ( - - - - )} - {radarr && ( - - - - )} - - - - - - - - ); -}; - -export default Router; diff --git a/frontend/src/Navigation/index.ts b/frontend/src/Navigation/index.ts new file mode 100644 index 000000000..0e63b9850 --- /dev/null +++ b/frontend/src/Navigation/index.ts @@ -0,0 +1,238 @@ +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"; + +export function useNavigationItems() { + const sonarr = useIsSonarrEnabled(); + const radarr = useIsRadarrEnabled(); + const { movies, episodes, providers } = useReduxStore((s) => s.site.badges); + + const items = useMemo( + () => [ + { + name: "404", + path: RouterEmptyPath, + component: EmptyPage, + 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", + badge: 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", + component: SystemProvidersView, + }, + { + name: "Status", + path: "/status", + component: SystemStatusView, + }, + { + name: "Releases", + path: "/releases", + component: SystemReleasesView, + }, + ], + }, + ], + [episodes, movies, providers, radarr, sonarr] + ); + + return items; +} diff --git a/frontend/src/Navigation/nav.d.ts b/frontend/src/Navigation/nav.d.ts new file mode 100644 index 000000000..7ce67f082 --- /dev/null +++ b/frontend/src/Navigation/nav.d.ts @@ -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; +} diff --git a/frontend/src/Router/index.tsx b/frontend/src/Router/index.tsx new file mode 100644 index 000000000..e9295db7f --- /dev/null +++ b/frontend/src/Router/index.tsx @@ -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 ( +
+ + {navItems.map((v, idx) => { + if ("routes" in v) { + return ( + + + + ); + } else if (v.enabled !== false) { + return ( + + ); + } else { + return null; + } + })} + + + + +
+ ); +}; + +export default Router; + +const ParentRouter: FunctionComponent = ({ + path, + enabled, + component, + routes, +}) => { + if (enabled === false || (component === undefined && routes.length === 0)) { + return null; + } + const ParentComponent = + component ?? (() => ); + + return ( + + + {routes + .filter((v) => v.enabled !== false) + .map((v, idx) => ( + + ))} + + + + + ); +}; diff --git a/frontend/src/Settings/Router.tsx b/frontend/src/Settings/Router.tsx deleted file mode 100644 index 5bf6828c5..000000000 --- a/frontend/src/Settings/Router.tsx +++ /dev/null @@ -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 = () => { - useSetSidebar("Settings"); - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default Router; diff --git a/frontend/src/Sidebar/index.tsx b/frontend/src/Sidebar/index.tsx index 00bc43f55..12af3d303 100644 --- a/frontend/src/Sidebar/index.tsx +++ b/frontend/src/Sidebar/index.tsx @@ -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 = ({ 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( - () => ({ - Wanted: { - Series: sonarrEnabled ? episodes : 0, - Movies: radarrEnabled ? movies : 0, - }, - System: { - Providers: providers, - Status: status, - }, - }), - [movies, episodes, providers, sonarrEnabled, radarrEnabled, status] - ); - - const hiddenKeys = useMemo(() => { - 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 ; - } else { - return ; - } - }), - [hiddenKeys] - ); - const goHome = useGotoHomepage(); + const [selection, setSelection] = useState(null); + return ( - + -
+
changeSidebar(false)} + >
+
+ ); +}; + +const SidebarNavigation: FunctionComponent = () => { + const navItems = useNavigationItems(); + + return ( + + {navItems.map((v, idx) => { + if ("routes" in v) { + return ( + + ); + } else { + return ( + + ); + } + })} + + ); +}; + +const SidebarParent: FunctionComponent = ({ + 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 ( + changeSidebar(false)} + > + + + ); + } else { + return null; + } + } + + return ( +
+ { + if (open) { + select(null); + } else { + select(path); + } + if (component !== undefined) { + history.push(path); + } + }} + > + + + +
+ {enabledRoutes.map((v, idx) => ( + + ))} +
+
+
+ ); +}; + +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 ( + { + select(null); + changeSidebar(false); + }} + > + + + ); +}; + +const SidebarContent: FunctionComponent<{ + icon?: IconDefinition; + name: string; + badge?: number; +}> = ({ icon, name, badge }) => { + return ( + + {icon && ( + + )} + + {name} {badge !== 0 ? badge : null} + ); }; diff --git a/frontend/src/Sidebar/items.tsx b/frontend/src/Sidebar/items.tsx deleted file mode 100644 index beb376eb2..000000000 --- a/frontend/src/Sidebar/items.tsx +++ /dev/null @@ -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([]); - -export const BadgesContext = React.createContext({}); - -function useToggleSidebar() { - return useReduxAction(siteChangeSidebar); -} - -function useSidebarKey() { - return useReduxStore((s) => s.site.sidebar); -} - -export const LinkItem: FunctionComponent = ({ - link, - name, - icon, -}) => { - const badges = useContext(BadgesContext); - const toggle = useContext(SidebarToggleContext); - - const badgeValue = useMemo(() => { - let badge: Nullable = null; - if (name in badges) { - let item = badges[name]; - if (typeof item === "number") { - badge = item; - } - } - return badge; - }, [badges, name]); - - return ( - - - - ); -}; - -export const CollapseItem: FunctionComponent = ({ - 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, Nullable] - >(() => { - let badge: Nullable = null; - let child: Nullable = 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 = null; - if (childValue && ch.name in childValue) { - badge = childValue[ch.name]; - } - return ( - - - - ); - }), - [children, hiddenKeys, childValue, toggleSidebar] - ); - - if (childrenElems.length === 0) { - return null; - } - - return ( -
- { - if (active) { - updateSidebar(""); - } else { - updateSidebar(name); - } - }} - > - - - -
{childrenElems}
-
-
- ); -}; - -interface DisplayProps { - name: string; - icon?: IconDefinition; - badge?: number; -} - -const DisplayItem: FunctionComponent = ({ - name, - icon, - badge, -}) => ( - - {icon && ( - - )} - - {name} {badge} - - -); diff --git a/frontend/src/Sidebar/list.ts b/frontend/src/Sidebar/list.ts deleted file mode 100644 index 9285f282e..000000000 --- a/frontend/src/Sidebar/list.ts +++ /dev/null @@ -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", - }, - ], - }, -]; diff --git a/frontend/src/Sidebar/types.d.ts b/frontend/src/Sidebar/types.d.ts deleted file mode 100644 index 7b7d27a22..000000000 --- a/frontend/src/Sidebar/types.d.ts +++ /dev/null @@ -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; -}; diff --git a/frontend/src/System/Releases/index.tsx b/frontend/src/System/Releases/index.tsx index 8df51c975..9c6f1cb93 100644 --- a/frontend/src/System/Releases/index.tsx +++ b/frontend/src/System/Releases/index.tsx @@ -7,7 +7,7 @@ import { BuildKey } from "../../utilities"; interface Props {} -const ReleasesView: FunctionComponent = () => { +const SystemReleasesView: FunctionComponent = () => { const releases = useSystemReleases(); return ( @@ -32,25 +32,6 @@ const ReleasesView: FunctionComponent = () => {
); - - // return ( - // - // {({ data }) => ( - // - // - // Releases - Bazarr (System) - // - // - // {data.map((v, idx) => ( - // - // - // - // ))} - // - // - // )} - // - // ); }; const headerBadgeCls = "mr-2"; @@ -95,4 +76,4 @@ const InfoElement: FunctionComponent = ({ ); }; -export default ReleasesView; +export default SystemReleasesView; diff --git a/frontend/src/System/Router.tsx b/frontend/src/System/Router.tsx deleted file mode 100644 index 575cf228b..000000000 --- a/frontend/src/System/Router.tsx +++ /dev/null @@ -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 ( - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default Router; diff --git a/frontend/src/Wanted/Router.tsx b/frontend/src/Wanted/Router.tsx deleted file mode 100644 index 750c18ee3..000000000 --- a/frontend/src/Wanted/Router.tsx +++ /dev/null @@ -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 ( - - {sonarr && ( - - - - )} - {radarr && ( - - - - )} - - - - - ); -}; - -export default Router; From 338f9c2bbec00bba54b1b8b6498eb5df34eedc6c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Sep 2021 00:56:37 +0800 Subject: [PATCH 02/13] no log: Bump sass from 1.37.5 to 1.38.2 in /frontend (#1523) Bumps [sass](https://github.com/sass/dart-sass) from 1.37.5 to 1.38.2. - [Release notes](https://github.com/sass/dart-sass/releases) - [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md) - [Commits](https://github.com/sass/dart-sass/compare/1.37.5...1.38.2) --- updated-dependencies: - dependency-name: sass dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d476e2cf5..0203bba61 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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" }, @@ -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" } From de5bcc8ed0054e0b7d53f1b7919cb2d2f9d0d94f Mon Sep 17 00:00:00 2001 From: Guy Khmelnitsky <3136012+GuyKh@users.noreply.github.com> Date: Tue, 31 Aug 2021 20:26:16 +0300 Subject: [PATCH 03/13] Added provider Ktuvit --- README.md | 1 + bazarr/config.py | 4 + bazarr/get_providers.py | 4 + frontend/src/@types/settings.d.ts | 6 + frontend/src/Settings/Providers/list.ts | 12 + libs/subliminal_patch/providers/ktuvit.py | 452 ++++++++++++++++++++++ 6 files changed, 479 insertions(+) create mode 100644 libs/subliminal_patch/providers/ktuvit.py diff --git a/README.md b/README.md index 6af69636f..532e6b68d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/bazarr/config.py b/bazarr/config.py index 5ce11a47c..78a8a6066 100644 --- a/bazarr/config.py +++ b/bazarr/config.py @@ -139,6 +139,10 @@ defaults = { 'password': '', 'skip_wrong_fps': 'False' }, + 'ktuvit': { + 'email': '', + 'hashed_password': '' + }, 'legendastv': { 'username': '', 'password': '', diff --git a/bazarr/get_providers.py b/bazarr/get_providers.py index b2b700f6b..44c7bc3a8 100644 --- a/bazarr/get_providers.py +++ b/bazarr/get_providers.py @@ -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, + }, } diff --git a/frontend/src/@types/settings.d.ts b/frontend/src/@types/settings.d.ts index c47e7edbf..8de53db86 100644 --- a/frontend/src/@types/settings.d.ts +++ b/frontend/src/@types/settings.d.ts @@ -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; } diff --git a/frontend/src/Settings/Providers/list.ts b/frontend/src/Settings/Providers/list.ts index 29d359741..0f249f9c1 100644 --- a/frontend/src/Settings/Providers/list.ts +++ b/frontend/src/Settings/Providers/list.ts @@ -70,6 +70,18 @@ export const ProviderList: Readonly = [ 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", diff --git a/libs/subliminal_patch/providers/ktuvit.py b/libs/subliminal_patch/providers/ktuvit.py new file mode 100644 index 000000000..d772914c9 --- /dev/null +++ b/libs/subliminal_patch/providers/ktuvit.py @@ -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) \ No newline at end of file From ccd5473598c4434ae8264a1ebdc957cdf6b3c947 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Aug 2021 13:51:52 -0400 Subject: [PATCH 04/13] [workflow]: Bump devmasx/merge-branch from 1.3.1 to 1.4.0 (#1524) Bumps [devmasx/merge-branch](https://github.com/devmasx/merge-branch) from 1.3.1 to 1.4.0. - [Release notes](https://github.com/devmasx/merge-branch/releases) - [Changelog](https://github.com/devmasx/merge-branch/blob/master/CHANGELOG.md) - [Commits](https://github.com/devmasx/merge-branch/compare/v1.3.1...1.4.0) --- updated-dependencies: - dependency-name: devmasx/merge-branch dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release_dev_to_master.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release_dev_to_master.yaml b/.github/workflows/release_dev_to_master.yaml index 0fd2b4aa3..5e241a66a 100644 --- a/.github/workflows/release_dev_to_master.yaml +++ b/.github/workflows/release_dev_to_master.yaml @@ -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 From 60e2732f48f2aa4f2ab1845b40cff8fef10e1bc2 Mon Sep 17 00:00:00 2001 From: LASER-Yi Date: Wed, 1 Sep 2021 11:30:28 +0800 Subject: [PATCH 05/13] Fix items are not updated when using mass editor --- frontend/src/components/header/Button.tsx | 10 +++------- frontend/src/components/inputs/Chips.tsx | 10 ++++------ 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/frontend/src/components/header/Button.tsx b/frontend/src/components/header/Button.tsx index 7036de021..fa0480689 100644 --- a/frontend/src/components/header/Button.tsx +++ b/frontend/src/components/header/Button.tsx @@ -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 Promise>( 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 ( = ({ const input = useRef(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 = ({ 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(() => { From 60de70f9e8db1216396cc9275077ece46036dc94 Mon Sep 17 00:00:00 2001 From: LASER-Yi Date: Wed, 1 Sep 2021 11:34:24 +0800 Subject: [PATCH 06/13] Fix incorrect position of provider badge --- frontend/src/Navigation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Navigation/index.ts b/frontend/src/Navigation/index.ts index 0e63b9850..ee9e6d559 100644 --- a/frontend/src/Navigation/index.ts +++ b/frontend/src/Navigation/index.ts @@ -163,7 +163,6 @@ export function useNavigationItems() { { name: "Providers", path: "/providers", - badge: providers, component: SettingsProvidersView, }, { @@ -216,6 +215,7 @@ export function useNavigationItems() { { name: "Providers", path: "/providers", + badge: providers, component: SystemProvidersView, }, { From 96a3acf8e945ca8a509a1b653a6d9d2622218eea Mon Sep 17 00:00:00 2001 From: morpheus65535 Date: Thu, 2 Sep 2021 19:07:08 -0400 Subject: [PATCH 07/13] Added movies searching to Addic7ed provider. #1525 --- libs/subliminal/providers/addic7ed.py | 31 ++-- libs/subliminal_patch/providers/addic7ed.py | 156 +++++++++++++++++++- 2 files changed, 173 insertions(+), 14 deletions(-) diff --git a/libs/subliminal/providers/addic7ed.py b/libs/subliminal/providers/addic7ed.py index 117784839..7c9ab1501 100644 --- a/libs/subliminal/providers/addic7ed.py +++ b/libs/subliminal/providers/addic7ed.py @@ -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) diff --git a/libs/subliminal_patch/providers/addic7ed.py b/libs/subliminal_patch/providers/addic7ed.py index 63a6b881f..f61222ee6 100644 --- a/libs/subliminal_patch/providers/addic7ed.py +++ b/libs/subliminal_patch/providers/addic7ed.py @@ -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[ \w\'.:(),*&!?-]+?)(?: \((?P\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,51 @@ 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 + if match := re.search(r'(.+)\s\((\d{4})\)$', media_title): + 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 +440,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}, From 9901bf340019d431c1aaba69546589e73ccbd8f3 Mon Sep 17 00:00:00 2001 From: LASER-Yi Date: Fri, 3 Sep 2021 09:35:46 +0800 Subject: [PATCH 08/13] Fix redirect issues when accessing root path --- frontend/src/Navigation/RootRedirect.tsx | 19 +++++++++++++++++++ frontend/src/Navigation/index.ts | 7 +++++++ 2 files changed, 26 insertions(+) create mode 100644 frontend/src/Navigation/RootRedirect.tsx diff --git a/frontend/src/Navigation/RootRedirect.tsx b/frontend/src/Navigation/RootRedirect.tsx new file mode 100644 index 000000000..eec9a335d --- /dev/null +++ b/frontend/src/Navigation/RootRedirect.tsx @@ -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 ; +}; + +export default RootRedirect; diff --git a/frontend/src/Navigation/index.ts b/frontend/src/Navigation/index.ts index ee9e6d559..dbcb4db6a 100644 --- a/frontend/src/Navigation/index.ts +++ b/frontend/src/Navigation/index.ts @@ -37,6 +37,7 @@ 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(); @@ -51,6 +52,12 @@ export function useNavigationItems() { component: EmptyPage, routeOnly: true, }, + { + name: "Redirect", + path: "/", + component: RootRedirect, + routeOnly: true, + }, { icon: faPlay, name: "Series", From 37e9e251f02be75bdbd58ef509dff7224b91add7 Mon Sep 17 00:00:00 2001 From: Dustin Wyatt Date: Fri, 3 Sep 2021 17:56:27 -0500 Subject: [PATCH 09/13] no log: Fix typo (#1527) --- frontend/src/DisplayItem/Episodes/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/DisplayItem/Episodes/index.tsx b/frontend/src/DisplayItem/Episodes/index.tsx index 6639dd2d9..0236ca42b 100644 --- a/frontend/src/DisplayItem/Episodes/index.tsx +++ b/frontend/src/DisplayItem/Episodes/index.tsx @@ -102,7 +102,7 @@ const SeriesEpisodesView: FunctionComponent = (props) => { seriesid: id, } ); - dispatchTask("Scaning disk...", [task], "Scaning..."); + dispatchTask("Scanning disk...", [task], "Scanning..."); }} > Scan Disk From caf01da2edadbbc847f39a92c45c67587e7eb152 Mon Sep 17 00:00:00 2001 From: morpheus65535 Date: Fri, 3 Sep 2021 19:19:02 -0400 Subject: [PATCH 10/13] Fixed incompatible operator with Python 3.7 in latest patch to Addic7ed. --- libs/subliminal_patch/providers/addic7ed.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/subliminal_patch/providers/addic7ed.py b/libs/subliminal_patch/providers/addic7ed.py index f61222ee6..530ab62dc 100644 --- a/libs/subliminal_patch/providers/addic7ed.py +++ b/libs/subliminal_patch/providers/addic7ed.py @@ -252,7 +252,8 @@ class Addic7edProvider(_Addic7edProvider): type, media_id = link['href'].split('/') if type == 'movie': media_title = link.text - if match := re.search(r'(.+)\s\((\d{4})\)$', media_title): + 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): From 8d5c2db39d8016ef9e69e42004e1b3f929360c44 Mon Sep 17 00:00:00 2001 From: morpheus65535 Date: Sat, 4 Sep 2021 21:22:58 -0400 Subject: [PATCH 11/13] Fixed Titrari providers search endpoint. #1526 --- libs/subliminal_patch/providers/titrari.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/subliminal_patch/providers/titrari.py b/libs/subliminal_patch/providers/titrari.py index 7ac00c3d7..615ceb122 100644 --- a/libs/subliminal_patch/providers/titrari.py +++ b/libs/subliminal_patch/providers/titrari.py @@ -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 From 560398b44c7cc48e3d518c3d6d12eb29a61b6ef7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Sep 2021 22:31:58 +0800 Subject: [PATCH 12/13] no log: Bump react-router-dom from 5.2.0 to 5.3.0 in /frontend (#1530) Bumps [react-router-dom](https://github.com/ReactTraining/react-router) from 5.2.0 to 5.3.0. - [Release notes](https://github.com/ReactTraining/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/CHANGELOG.md) - [Commits](https://github.com/ReactTraining/react-router/compare/v5.2.0...v5.3.0) --- updated-dependencies: - dependency-name: react-router-dom dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package-lock.json | 38 +++++++++++++++++++------------------- frontend/package.json | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0203bba61..76a175d63 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -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" }, @@ -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" } diff --git a/frontend/package.json b/frontend/package.json index 3635eb800..09be6fb60 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", From 35cb757df80dbd59d781a4cf594aa8fcb8fd66b5 Mon Sep 17 00:00:00 2001 From: Matheus Horstmann Date: Thu, 9 Sep 2021 23:51:16 -0300 Subject: [PATCH 13/13] Resolved problem in virtualenv detection Fix #1535 --- bazarr/init.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bazarr/init.py b/bazarr/init.py index 928c44f3d..6560ad911 100644 --- a/bazarr/init.py +++ b/bazarr/init.py @@ -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