pull/2066/head
v1.1.5-beta.20
parent
52df29a1f5
commit
58262bc299
@ -0,0 +1,35 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
|
||||
from app.announcements import get_all_announcements, mark_announcement_as_dismissed
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_system_announcements = Namespace('System Announcements', description='List announcements relative to Bazarr')
|
||||
|
||||
|
||||
@api_ns_system_announcements.route('system/announcements')
|
||||
class SystemAnnouncements(Resource):
|
||||
@authenticate
|
||||
@api_ns_system_announcements.doc(parser=None)
|
||||
@api_ns_system_announcements.response(200, 'Success')
|
||||
@api_ns_system_announcements.response(401, 'Not Authenticated')
|
||||
def get(self):
|
||||
"""List announcements relative to Bazarr"""
|
||||
return {'data': get_all_announcements()}
|
||||
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('hash', type=str, required=True, help='hash of the announcement to dismiss')
|
||||
|
||||
@authenticate
|
||||
@api_ns_system_announcements.doc(parser=post_request_parser)
|
||||
@api_ns_system_announcements.response(204, 'Success')
|
||||
@api_ns_system_announcements.response(401, 'Not Authenticated')
|
||||
def post(self):
|
||||
"""Mark announcement as dismissed"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
hashed_announcement = args.get('hash')
|
||||
|
||||
mark_announcement_as_dismissed(hashed_announcement=hashed_announcement)
|
||||
return '', 204
|
@ -0,0 +1,113 @@
|
||||
# coding=utf-8
|
||||
|
||||
import os
|
||||
import hashlib
|
||||
import requests
|
||||
import logging
|
||||
import json
|
||||
import pretty
|
||||
|
||||
from datetime import datetime
|
||||
from operator import itemgetter
|
||||
|
||||
from app.get_providers import get_providers
|
||||
from app.database import TableAnnouncements
|
||||
from .get_args import args
|
||||
|
||||
|
||||
# Announcements as receive by browser must be in the form of a list of dicts converted to JSON
|
||||
# [
|
||||
# {
|
||||
# 'text': 'some text',
|
||||
# 'link': 'http://to.somewhere.net',
|
||||
# 'hash': '',
|
||||
# 'dismissible': True,
|
||||
# 'timestamp': 1676236978,
|
||||
# 'enabled': True,
|
||||
# },
|
||||
# ]
|
||||
|
||||
|
||||
def parse_announcement_dict(announcement_dict):
|
||||
announcement_dict['timestamp'] = pretty.date(announcement_dict['timestamp'])
|
||||
announcement_dict['link'] = announcement_dict.get('link', '')
|
||||
announcement_dict['dismissible'] = announcement_dict.get('dismissible', True)
|
||||
announcement_dict['enabled'] = announcement_dict.get('enabled', True)
|
||||
announcement_dict['hash'] = hashlib.sha256(announcement_dict['text'].encode('UTF8')).hexdigest()
|
||||
|
||||
return announcement_dict
|
||||
|
||||
|
||||
def get_announcements_to_file():
|
||||
try:
|
||||
r = requests.get("https://raw.githubusercontent.com/morpheus65535/bazarr-binaries/master/announcements.json")
|
||||
except requests.exceptions.HTTPError:
|
||||
logging.exception("Error trying to get announcements from Github. Http error.")
|
||||
except requests.exceptions.ConnectionError:
|
||||
logging.exception("Error trying to get announcements from Github. Connection Error.")
|
||||
except requests.exceptions.Timeout:
|
||||
logging.exception("Error trying to get announcements from Github. Timeout Error.")
|
||||
except requests.exceptions.RequestException:
|
||||
logging.exception("Error trying to get announcements from Github.")
|
||||
else:
|
||||
with open(os.path.join(args.config_dir, 'config', 'announcements.json'), 'wb') as f:
|
||||
f.write(r.content)
|
||||
|
||||
|
||||
def get_online_announcements():
|
||||
try:
|
||||
with open(os.path.join(args.config_dir, 'config', 'announcements.json'), 'r') as f:
|
||||
data = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return []
|
||||
else:
|
||||
for announcement in data['data']:
|
||||
if 'enabled' not in announcement:
|
||||
data['data'][announcement]['enabled'] = True
|
||||
if 'dismissible' not in announcement:
|
||||
data['data'][announcement]['dismissible'] = True
|
||||
|
||||
return data['data']
|
||||
|
||||
|
||||
def get_local_announcements():
|
||||
announcements = []
|
||||
|
||||
# opensubtitles.org end-of-life
|
||||
enabled_providers = get_providers()
|
||||
if enabled_providers and 'opensubtitles' in enabled_providers:
|
||||
announcements.append({
|
||||
'text': 'Opensubtitles.org will be deprecated soon, migrate to Opensubtitles.com ASAP and disable this '
|
||||
'provider to remove this announcement.',
|
||||
'link': 'https://wiki.bazarr.media/Troubleshooting/OpenSubtitles-migration/',
|
||||
'dismissible': False,
|
||||
'timestamp': 1676236978,
|
||||
})
|
||||
|
||||
for announcement in announcements:
|
||||
if 'enabled' not in announcement:
|
||||
announcement['enabled'] = True
|
||||
if 'dismissible' not in announcement:
|
||||
announcement['dismissible'] = True
|
||||
|
||||
return announcements
|
||||
|
||||
|
||||
def get_all_announcements():
|
||||
# get announcements that haven't been dismissed yet
|
||||
announcements = [parse_announcement_dict(x) for x in get_online_announcements() + get_local_announcements() if
|
||||
x['enabled'] and (not x['dismissible'] or not TableAnnouncements.select()
|
||||
.where(TableAnnouncements.hash ==
|
||||
hashlib.sha256(x['text'].encode('UTF8')).hexdigest()).get_or_none())]
|
||||
|
||||
return sorted(announcements, key=itemgetter('timestamp'), reverse=True)
|
||||
|
||||
|
||||
def mark_announcement_as_dismissed(hashed_announcement):
|
||||
text = [x['text'] for x in get_all_announcements() if x['hash'] == hashed_announcement]
|
||||
if text:
|
||||
TableAnnouncements.insert({TableAnnouncements.hash: hashed_announcement,
|
||||
TableAnnouncements.timestamp: datetime.now(),
|
||||
TableAnnouncements.text: text[0]})\
|
||||
.on_conflict_ignore(ignore=True)\
|
||||
.execute()
|
@ -0,0 +1,24 @@
|
||||
import { useSystemAnnouncements } from "@/apis/hooks";
|
||||
import { QueryOverlay } from "@/components/async";
|
||||
import { Container } from "@mantine/core";
|
||||
import { useDocumentTitle } from "@mantine/hooks";
|
||||
import { FunctionComponent } from "react";
|
||||
import Table from "./table";
|
||||
|
||||
const SystemAnnouncementsView: FunctionComponent = () => {
|
||||
const announcements = useSystemAnnouncements();
|
||||
|
||||
const { data } = announcements;
|
||||
|
||||
useDocumentTitle("Announcements - Bazarr (System)");
|
||||
|
||||
return (
|
||||
<QueryOverlay result={announcements}>
|
||||
<Container fluid px={0}>
|
||||
<Table announcements={data ?? []}></Table>
|
||||
</Container>
|
||||
</QueryOverlay>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemAnnouncementsView;
|
@ -0,0 +1,91 @@
|
||||
import { useSystemAnnouncementsAddDismiss } from "@/apis/hooks";
|
||||
import { SimpleTable } from "@/components";
|
||||
import { MutateAction } from "@/components/async";
|
||||
import { useTableStyles } from "@/styles";
|
||||
import { faWindowClose } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Anchor, Text } from "@mantine/core";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
import { Column } from "react-table";
|
||||
|
||||
interface Props {
|
||||
announcements: readonly System.Announcements[];
|
||||
}
|
||||
|
||||
const Table: FunctionComponent<Props> = ({ announcements }) => {
|
||||
const columns: Column<System.Announcements>[] = useMemo<
|
||||
Column<System.Announcements>[]
|
||||
>(
|
||||
() => [
|
||||
{
|
||||
Header: "Since",
|
||||
accessor: "timestamp",
|
||||
Cell: ({ value }) => {
|
||||
const { classes } = useTableStyles();
|
||||
return <Text className={classes.primary}>{value}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Announcement",
|
||||
accessor: "text",
|
||||
Cell: ({ value }) => {
|
||||
const { classes } = useTableStyles();
|
||||
return <Text className={classes.primary}>{value}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "More info",
|
||||
accessor: "link",
|
||||
Cell: ({ value }) => {
|
||||
if (value) {
|
||||
return <Label link={value}>Link</Label>;
|
||||
} else {
|
||||
return <Text>n/a</Text>;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
Header: "Dismiss",
|
||||
accessor: "hash",
|
||||
Cell: ({ row, value }) => {
|
||||
const add = useSystemAnnouncementsAddDismiss();
|
||||
return (
|
||||
<MutateAction
|
||||
label="Dismiss announcement"
|
||||
disabled={!row.original.dismissible}
|
||||
icon={faWindowClose}
|
||||
mutation={add}
|
||||
args={() => ({
|
||||
hash: value,
|
||||
})}
|
||||
></MutateAction>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<SimpleTable
|
||||
columns={columns}
|
||||
data={announcements}
|
||||
tableStyles={{ emptyText: "No announcements for now, come back later!" }}
|
||||
></SimpleTable>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
|
||||
interface LabelProps {
|
||||
link: string;
|
||||
children: string;
|
||||
}
|
||||
|
||||
function Label(props: LabelProps): JSX.Element {
|
||||
const { link, children } = props;
|
||||
return (
|
||||
<Anchor href={link} target="_blank" rel="noopener noreferrer">
|
||||
{children}
|
||||
</Anchor>
|
||||
);
|
||||
}
|
Loading…
Reference in new issue