mirror of https://github.com/morpheus65535/bazarr
Merge development into master
This commit is contained in:
commit
70346950fd
|
@ -16,13 +16,13 @@ file_list=$(cat .github/files_to_copy)
|
|||
for f in $file_list
|
||||
do
|
||||
echo "**** copying $f to release ****"
|
||||
cp -r --parents $f $to_dist
|
||||
cp -r --parents "$f" $to_dist
|
||||
done
|
||||
|
||||
# COPY VERSION file
|
||||
cp VERSION $to_dist
|
||||
|
||||
pushd __builds__/bazarr
|
||||
zip -r ../bazarr.zip . -b $(mktemp -d)
|
||||
zip -r ../bazarr.zip . -b "$(mktemp -d)"
|
||||
popd
|
||||
rm -rf $to_dist
|
|
@ -8,7 +8,7 @@ master_version=$(git describe --tags --abbrev=0 --match "v[0-9].[0-9].[0-9]")
|
|||
latest_verion=$(git describe --tags --abbrev=0)
|
||||
|
||||
if [[ $RELEASE_MASTER -eq 1 ]]; then
|
||||
auto-changelog --stdout -t changelog-master.hbs --starting-version $master_version --commit-limit 3
|
||||
auto-changelog --stdout -t changelog-master.hbs --starting-version "$master_version" --commit-limit 3
|
||||
else
|
||||
auto-changelog --stdout --starting-version $latest_verion --unreleased --commit-limit 0
|
||||
auto-changelog --stdout --starting-version "$latest_verion" --unreleased --commit-limit 0
|
||||
fi
|
|
@ -9,7 +9,7 @@ file_list=$(cat .github/files_to_copy)
|
|||
for f in $file_list
|
||||
do
|
||||
echo "**** checking $f ****"
|
||||
if [ ! -f $f ] && [ ! -d $f ]; then
|
||||
if [ ! -f "$f" ] && [ ! -d "$f" ]; then
|
||||
echo "**** $f doesn't exist, skipping release ****"
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
@ -1,25 +1,51 @@
|
|||
# coding=utf-8
|
||||
|
||||
from .badges import api_bp_badges
|
||||
from .system import api_bp_system
|
||||
from .series import api_bp_series
|
||||
from .episodes import api_bp_episodes
|
||||
from .providers import api_bp_providers
|
||||
from .subtitles import api_bp_subtitles
|
||||
from .webhooks import api_bp_webhooks
|
||||
from .history import api_bp_history
|
||||
from .files import api_bp_files
|
||||
from .movies import api_bp_movies
|
||||
from flask import Blueprint, url_for
|
||||
from flask_restx import Api, apidoc
|
||||
|
||||
api_bp_list = [
|
||||
api_bp_badges,
|
||||
api_bp_system,
|
||||
api_bp_series,
|
||||
api_bp_episodes,
|
||||
api_bp_providers,
|
||||
api_bp_subtitles,
|
||||
api_bp_webhooks,
|
||||
api_bp_history,
|
||||
api_bp_files,
|
||||
api_bp_movies
|
||||
from .badges import api_ns_list_badges
|
||||
from .episodes import api_ns_list_episodes
|
||||
from .files import api_ns_list_files
|
||||
from .history import api_ns_list_history
|
||||
from .movies import api_ns_list_movies
|
||||
from .providers import api_ns_list_providers
|
||||
from .series import api_ns_list_series
|
||||
from .subtitles import api_ns_list_subtitles
|
||||
from .system import api_ns_list_system
|
||||
from .webhooks import api_ns_list_webhooks
|
||||
from .swaggerui import swaggerui_api_params
|
||||
|
||||
api_ns_list = [
|
||||
api_ns_list_badges,
|
||||
api_ns_list_episodes,
|
||||
api_ns_list_files,
|
||||
api_ns_list_history,
|
||||
api_ns_list_movies,
|
||||
api_ns_list_providers,
|
||||
api_ns_list_series,
|
||||
api_ns_list_subtitles,
|
||||
api_ns_list_system,
|
||||
api_ns_list_webhooks,
|
||||
]
|
||||
|
||||
authorizations = {
|
||||
'apikey': {
|
||||
'type': 'apiKey',
|
||||
'in': 'header',
|
||||
'name': 'X-API-KEY'
|
||||
}
|
||||
}
|
||||
|
||||
api_bp = Blueprint('api', __name__, url_prefix='/api')
|
||||
|
||||
|
||||
@apidoc.apidoc.add_app_template_global
|
||||
def swagger_static(filename):
|
||||
return url_for('ui.swaggerui_static', filename=filename)
|
||||
|
||||
|
||||
api = Api(api_bp, authorizations=authorizations, security='apikey', validate=True, **swaggerui_api_params)
|
||||
|
||||
for api_ns in api_ns_list:
|
||||
for item in api_ns:
|
||||
api.add_namespace(item, "/")
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
from .badges import api_ns_badges
|
||||
|
||||
from .badges import Badges
|
||||
|
||||
|
||||
api_bp_badges = Blueprint('api_badges', __name__)
|
||||
api = Api(api_bp_badges)
|
||||
|
||||
api.add_resource(Badges, '/badges')
|
||||
api_ns_list_badges = [
|
||||
api_ns_badges
|
||||
]
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
import operator
|
||||
|
||||
from functools import reduce
|
||||
from flask import jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, fields
|
||||
|
||||
from app.database import get_exclusion_clause, TableEpisodes, TableShows, TableMovies
|
||||
from app.get_providers import get_throttled_providers
|
||||
|
@ -12,10 +11,25 @@ from utilities.health import get_health_issues
|
|||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_badges = Namespace('Badges', description='Get badges count to update the UI (episodes and movies wanted '
|
||||
'subtitles, providers with issues and health issues.')
|
||||
|
||||
|
||||
@api_ns_badges.route('badges')
|
||||
class Badges(Resource):
|
||||
get_model = api_ns_badges.model('BadgesGet', {
|
||||
'episodes': fields.Integer(),
|
||||
'movies': fields.Integer(),
|
||||
'providers': fields.Integer(),
|
||||
'status': fields.Integer(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_badges.marshal_with(get_model, code=200)
|
||||
@api_ns_badges.response(401, 'Not Authenticated')
|
||||
@api_ns_badges.doc(parser=None)
|
||||
def get(self):
|
||||
"""Get badges count to update the UI"""
|
||||
episodes_conditions = [(TableEpisodes.missing_subtitles.is_null(False)),
|
||||
(TableEpisodes.missing_subtitles != '[]')]
|
||||
episodes_conditions += get_exclusion_clause('series')
|
||||
|
@ -44,4 +58,4 @@ class Badges(Resource):
|
|||
"providers": throttled_providers,
|
||||
"status": health_issues
|
||||
}
|
||||
return jsonify(result)
|
||||
return result
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .episodes import Episodes
|
||||
from .episodes_subtitles import EpisodesSubtitles
|
||||
from .history import EpisodesHistory
|
||||
from .wanted import EpisodesWanted
|
||||
from .blacklist import EpisodesBlacklist
|
||||
from .episodes import api_ns_episodes
|
||||
from .episodes_subtitles import api_ns_episodes_subtitles
|
||||
from .history import api_ns_episodes_history
|
||||
from .wanted import api_ns_episodes_wanted
|
||||
from .blacklist import api_ns_episodes_blacklist
|
||||
|
||||
|
||||
api_bp_episodes = Blueprint('api_episodes', __name__)
|
||||
api = Api(api_bp_episodes)
|
||||
|
||||
api.add_resource(Episodes, '/episodes')
|
||||
api.add_resource(EpisodesWanted, '/episodes/wanted')
|
||||
api.add_resource(EpisodesSubtitles, '/episodes/subtitles')
|
||||
api.add_resource(EpisodesHistory, '/episodes/history')
|
||||
api.add_resource(EpisodesBlacklist, '/episodes/blacklist')
|
||||
api_ns_list_episodes = [
|
||||
api_ns_episodes,
|
||||
api_ns_episodes_blacklist,
|
||||
api_ns_episodes_history,
|
||||
api_ns_episodes_subtitles,
|
||||
api_ns_episodes_wanted,
|
||||
]
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
import datetime
|
||||
import pretty
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
|
||||
from app.database import TableEpisodes, TableShows, TableBlacklist
|
||||
from subtitles.tools.delete import delete_subtitles
|
||||
|
@ -12,18 +11,43 @@ from sonarr.blacklist import blacklist_log, blacklist_delete_all, blacklist_dele
|
|||
from utilities.path_mappings import path_mappings
|
||||
from subtitles.mass_download import episode_download_subtitles
|
||||
from app.event_handler import event_stream
|
||||
from api.swaggerui import subtitles_language_model
|
||||
|
||||
from ..utils import authenticate, postprocessEpisode
|
||||
|
||||
api_ns_episodes_blacklist = Namespace('Episodes Blacklist', description='List, add or remove subtitles to or from '
|
||||
'episodes blacklist')
|
||||
|
||||
# GET: get blacklist
|
||||
# POST: add blacklist
|
||||
# DELETE: remove blacklist
|
||||
|
||||
@api_ns_episodes_blacklist.route('episodes/blacklist')
|
||||
class EpisodesBlacklist(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('start', type=int, required=False, default=0, help='Paging start integer')
|
||||
get_request_parser.add_argument('length', type=int, required=False, default=-1, help='Paging length integer')
|
||||
|
||||
get_language_model = api_ns_episodes_blacklist.model('subtitles_language_model', subtitles_language_model)
|
||||
|
||||
get_response_model = api_ns_episodes_blacklist.model('EpisodeBlacklistGetResponse', {
|
||||
'seriesTitle': fields.String(),
|
||||
'episode_number': fields.String(),
|
||||
'episodeTitle': fields.String(),
|
||||
'sonarrSeriesId': fields.Integer(),
|
||||
'provider': fields.String(),
|
||||
'subs_id': fields.String(),
|
||||
'language': fields.Nested(get_language_model),
|
||||
'timestamp': fields.String(),
|
||||
'parsed_timestamp': fields.String(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_episodes_blacklist.marshal_with(get_response_model, envelope='data', code=200)
|
||||
@api_ns_episodes_blacklist.response(401, 'Not Authenticated')
|
||||
@api_ns_episodes_blacklist.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
"""List blacklisted episodes subtitles"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
start = args.get('start')
|
||||
length = args.get('length')
|
||||
|
||||
data = TableBlacklist.select(TableShows.title.alias('seriesTitle'),
|
||||
TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'),
|
||||
|
@ -48,15 +72,29 @@ class EpisodesBlacklist(Resource):
|
|||
|
||||
postprocessEpisode(item)
|
||||
|
||||
return jsonify(data=data)
|
||||
return data
|
||||
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('seriesid', type=int, required=True, help='Series ID')
|
||||
post_request_parser.add_argument('episodeid', type=int, required=True, help='Episode ID')
|
||||
post_request_parser.add_argument('provider', type=str, required=True, help='Provider name')
|
||||
post_request_parser.add_argument('subs_id', type=str, required=True, help='Subtitles ID')
|
||||
post_request_parser.add_argument('language', type=str, required=True, help='Subtitles language')
|
||||
post_request_parser.add_argument('subtitles_path', type=str, required=True, help='Subtitles file path')
|
||||
|
||||
@authenticate
|
||||
@api_ns_episodes_blacklist.doc(parser=post_request_parser)
|
||||
@api_ns_episodes_blacklist.response(200, 'Success')
|
||||
@api_ns_episodes_blacklist.response(401, 'Not Authenticated')
|
||||
@api_ns_episodes_blacklist.response(404, 'Episode not found')
|
||||
def post(self):
|
||||
sonarr_series_id = int(request.args.get('seriesid'))
|
||||
sonarr_episode_id = int(request.args.get('episodeid'))
|
||||
provider = request.form.get('provider')
|
||||
subs_id = request.form.get('subs_id')
|
||||
language = request.form.get('language')
|
||||
"""Add an episodes subtitles to blacklist"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
sonarr_series_id = args.get('seriesid')
|
||||
sonarr_episode_id = args.get('episodeid')
|
||||
provider = args.get('provider')
|
||||
subs_id = args.get('subs_id')
|
||||
language = args.get('language')
|
||||
|
||||
episodeInfo = TableEpisodes.select(TableEpisodes.path)\
|
||||
.where(TableEpisodes.sonarrEpisodeId == sonarr_episode_id)\
|
||||
|
@ -67,7 +105,7 @@ class EpisodesBlacklist(Resource):
|
|||
return 'Episode not found', 404
|
||||
|
||||
media_path = episodeInfo['path']
|
||||
subtitles_path = request.form.get('subtitles_path')
|
||||
subtitles_path = args.get('subtitles_path')
|
||||
|
||||
blacklist_log(sonarr_series_id=sonarr_series_id,
|
||||
sonarr_episode_id=sonarr_episode_id,
|
||||
|
@ -86,12 +124,22 @@ class EpisodesBlacklist(Resource):
|
|||
event_stream(type='episode-history')
|
||||
return '', 200
|
||||
|
||||
delete_request_parser = reqparse.RequestParser()
|
||||
delete_request_parser.add_argument('all', type=str, required=False, help='Empty episodes subtitles blacklist')
|
||||
delete_request_parser.add_argument('provider', type=str, required=True, help='Provider name')
|
||||
delete_request_parser.add_argument('subs_id', type=str, required=True, help='Subtitles ID')
|
||||
|
||||
@authenticate
|
||||
@api_ns_episodes_blacklist.doc(parser=delete_request_parser)
|
||||
@api_ns_episodes_blacklist.response(204, 'Success')
|
||||
@api_ns_episodes_blacklist.response(401, 'Not Authenticated')
|
||||
def delete(self):
|
||||
if request.args.get("all") == "true":
|
||||
"""Delete an episodes subtitles from blacklist"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
if args.get("all") == "true":
|
||||
blacklist_delete_all()
|
||||
else:
|
||||
provider = request.form.get('provider')
|
||||
subs_id = request.form.get('subs_id')
|
||||
provider = args.get('provider')
|
||||
subs_id = args.get('subs_id')
|
||||
blacklist_delete(provider=provider, subs_id=subs_id)
|
||||
return '', 204
|
||||
|
|
|
@ -1,18 +1,60 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
|
||||
from app.database import TableEpisodes
|
||||
from api.swaggerui import subtitles_model, subtitles_language_model, audio_language_model
|
||||
|
||||
from ..utils import authenticate, postprocessEpisode
|
||||
|
||||
api_ns_episodes = Namespace('Episodes', description='List episodes metadata for specific series or episodes.')
|
||||
|
||||
|
||||
@api_ns_episodes.route('episodes')
|
||||
class Episodes(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('seriesid[]', type=int, action='append', required=False, default=[],
|
||||
help='Series IDs to list episodes for')
|
||||
get_request_parser.add_argument('episodeid[]', type=int, action='append', required=False, default=[],
|
||||
help='Episodes ID to list')
|
||||
|
||||
get_subtitles_model = api_ns_episodes.model('subtitles_model', subtitles_model)
|
||||
get_subtitles_language_model = api_ns_episodes.model('subtitles_language_model', subtitles_language_model)
|
||||
get_audio_language_model = api_ns_episodes.model('audio_language_model', audio_language_model)
|
||||
|
||||
get_response_model = api_ns_episodes.model('EpisodeGetResponse', {
|
||||
'rowid': fields.Integer(),
|
||||
'audio_codec': fields.String(),
|
||||
'audio_language': fields.Nested(get_audio_language_model),
|
||||
'episode': fields.Integer(),
|
||||
'episode_file_id': fields.Integer(),
|
||||
'failedAttempts': fields.String(),
|
||||
'file_size': fields.Integer(),
|
||||
'format': fields.String(),
|
||||
'missing_subtitles': fields.Nested(get_subtitles_language_model),
|
||||
'monitored': fields.Boolean(),
|
||||
'path': fields.String(),
|
||||
'resolution': fields.String(),
|
||||
'season': fields.Integer(),
|
||||
'sonarrEpisodeId': fields.Integer(),
|
||||
'sonarrSeriesId': fields.Integer(),
|
||||
'subtitles': fields.Nested(get_subtitles_model),
|
||||
'title': fields.String(),
|
||||
'video_codec': fields.String(),
|
||||
'sceneName': fields.String(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_episodes.marshal_with(get_response_model, envelope='data', code=200)
|
||||
@api_ns_episodes.doc(parser=get_request_parser)
|
||||
@api_ns_episodes.response(200, 'Success')
|
||||
@api_ns_episodes.response(401, 'Not Authenticated')
|
||||
@api_ns_episodes.response(404, 'Series or Episode ID not provided')
|
||||
def get(self):
|
||||
seriesId = request.args.getlist('seriesid[]')
|
||||
episodeId = request.args.getlist('episodeid[]')
|
||||
"""List episodes metadata for specific series or episodes"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
seriesId = args.get('seriesid[]')
|
||||
episodeId = args.get('episodeid[]')
|
||||
|
||||
if len(episodeId) > 0:
|
||||
result = TableEpisodes.select().where(TableEpisodes.sonarrEpisodeId.in_(episodeId)).dicts()
|
||||
|
@ -28,4 +70,4 @@ class Episodes(Resource):
|
|||
for item in result:
|
||||
postprocessEpisode(item)
|
||||
|
||||
return jsonify(data=result)
|
||||
return result
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
import os
|
||||
import logging
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
from subliminal_patch.core import SUBTITLE_EXTENSIONS
|
||||
from werkzeug.datastructures import FileStorage
|
||||
|
||||
from app.database import TableShows, TableEpisodes, get_audio_profile_languages, get_profile_id
|
||||
from utilities.path_mappings import path_mappings
|
||||
|
@ -20,15 +20,28 @@ from app.config import settings
|
|||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_episodes_subtitles = Namespace('Episodes Subtitles', description='Download, upload or delete episodes subtitles')
|
||||
|
||||
# PATCH: Download Subtitles
|
||||
# POST: Upload Subtitles
|
||||
# DELETE: Delete Subtitles
|
||||
|
||||
@api_ns_episodes_subtitles.route('episodes/subtitles')
|
||||
class EpisodesSubtitles(Resource):
|
||||
patch_request_parser = reqparse.RequestParser()
|
||||
patch_request_parser.add_argument('seriesid', type=int, required=True, help='Series ID')
|
||||
patch_request_parser.add_argument('episodeid', type=int, required=True, help='Episode ID')
|
||||
patch_request_parser.add_argument('language', type=str, required=True, help='Language code2')
|
||||
patch_request_parser.add_argument('forced', type=str, required=True, help='Forced true/false as string')
|
||||
patch_request_parser.add_argument('hi', type=str, required=True, help='HI true/false as string')
|
||||
|
||||
@authenticate
|
||||
@api_ns_episodes_subtitles.doc(parser=patch_request_parser)
|
||||
@api_ns_episodes_subtitles.response(204, 'Success')
|
||||
@api_ns_episodes_subtitles.response(401, 'Not Authenticated')
|
||||
@api_ns_episodes_subtitles.response(404, 'Episode not found')
|
||||
def patch(self):
|
||||
sonarrSeriesId = request.args.get('seriesid')
|
||||
sonarrEpisodeId = request.args.get('episodeid')
|
||||
"""Download an episode subtitles"""
|
||||
args = self.patch_request_parser.parse_args()
|
||||
sonarrSeriesId = args.get('seriesid')
|
||||
sonarrEpisodeId = args.get('episodeid')
|
||||
episodeInfo = TableEpisodes.select(TableEpisodes.path,
|
||||
TableEpisodes.scene_name,
|
||||
TableEpisodes.audio_language,
|
||||
|
@ -45,9 +58,9 @@ class EpisodesSubtitles(Resource):
|
|||
episodePath = path_mappings.path_replace(episodeInfo['path'])
|
||||
sceneName = episodeInfo['scene_name'] or "None"
|
||||
|
||||
language = request.form.get('language')
|
||||
hi = request.form.get('hi').capitalize()
|
||||
forced = request.form.get('forced').capitalize()
|
||||
language = args.get('language')
|
||||
hi = args.get('hi').capitalize()
|
||||
forced = args.get('forced').capitalize()
|
||||
|
||||
audio_language_list = get_audio_profile_languages(episode_id=sonarrEpisodeId)
|
||||
if len(audio_language_list) > 0:
|
||||
|
@ -85,10 +98,25 @@ class EpisodesSubtitles(Resource):
|
|||
|
||||
return '', 204
|
||||
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('seriesid', type=int, required=True, help='Series ID')
|
||||
post_request_parser.add_argument('episodeid', type=int, required=True, help='Episode ID')
|
||||
post_request_parser.add_argument('language', type=str, required=True, help='Language code2')
|
||||
post_request_parser.add_argument('forced', type=str, required=True, help='Forced true/false as string')
|
||||
post_request_parser.add_argument('hi', type=str, required=True, help='HI true/false as string')
|
||||
post_request_parser.add_argument('file', type=FileStorage, location='files', required=True,
|
||||
help='Subtitles file as file upload object')
|
||||
|
||||
@authenticate
|
||||
@api_ns_episodes_subtitles.doc(parser=post_request_parser)
|
||||
@api_ns_episodes_subtitles.response(204, 'Success')
|
||||
@api_ns_episodes_subtitles.response(401, 'Not Authenticated')
|
||||
@api_ns_episodes_subtitles.response(404, 'Episode not found')
|
||||
def post(self):
|
||||
sonarrSeriesId = request.args.get('seriesid')
|
||||
sonarrEpisodeId = request.args.get('episodeid')
|
||||
"""Upload an episode subtitles"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
sonarrSeriesId = args.get('seriesid')
|
||||
sonarrEpisodeId = args.get('episodeid')
|
||||
episodeInfo = TableEpisodes.select(TableEpisodes.title,
|
||||
TableEpisodes.path,
|
||||
TableEpisodes.scene_name,
|
||||
|
@ -105,10 +133,10 @@ class EpisodesSubtitles(Resource):
|
|||
sceneName = episodeInfo['scene_name'] or "None"
|
||||
audio_language = episodeInfo['audio_language']
|
||||
|
||||
language = request.form.get('language')
|
||||
forced = True if request.form.get('forced') == 'true' else False
|
||||
hi = True if request.form.get('hi') == 'true' else False
|
||||
subFile = request.files.get('file')
|
||||
language = args.get('language')
|
||||
forced = True if args.get('forced') == 'true' else False
|
||||
hi = True if args.get('hi') == 'true' else False
|
||||
subFile = args.get('file')
|
||||
|
||||
_, ext = os.path.splitext(subFile.filename)
|
||||
|
||||
|
@ -151,10 +179,24 @@ class EpisodesSubtitles(Resource):
|
|||
|
||||
return '', 204
|
||||
|
||||
delete_request_parser = reqparse.RequestParser()
|
||||
delete_request_parser.add_argument('seriesid', type=int, required=True, help='Series ID')
|
||||
delete_request_parser.add_argument('episodeid', type=int, required=True, help='Episode ID')
|
||||
delete_request_parser.add_argument('language', type=str, required=True, help='Language code2')
|
||||
delete_request_parser.add_argument('forced', type=str, required=True, help='Forced true/false as string')
|
||||
delete_request_parser.add_argument('hi', type=str, required=True, help='HI true/false as string')
|
||||
delete_request_parser.add_argument('path', type=str, required=True, help='Path of the subtitles file')
|
||||
|
||||
@authenticate
|
||||
@api_ns_episodes_subtitles.doc(parser=delete_request_parser)
|
||||
@api_ns_episodes_subtitles.response(204, 'Success')
|
||||
@api_ns_episodes_subtitles.response(401, 'Not Authenticated')
|
||||
@api_ns_episodes_subtitles.response(404, 'Episode not found')
|
||||
def delete(self):
|
||||
sonarrSeriesId = request.args.get('seriesid')
|
||||
sonarrEpisodeId = request.args.get('episodeid')
|
||||
"""Delete an episode subtitles"""
|
||||
args = self.delete_request_parser.parse_args()
|
||||
sonarrSeriesId = args.get('seriesid')
|
||||
sonarrEpisodeId = args.get('episodeid')
|
||||
episodeInfo = TableEpisodes.select(TableEpisodes.title,
|
||||
TableEpisodes.path,
|
||||
TableEpisodes.scene_name,
|
||||
|
@ -168,10 +210,10 @@ class EpisodesSubtitles(Resource):
|
|||
|
||||
episodePath = path_mappings.path_replace(episodeInfo['path'])
|
||||
|
||||
language = request.form.get('language')
|
||||
forced = request.form.get('forced')
|
||||
hi = request.form.get('hi')
|
||||
subtitlesPath = request.form.get('path')
|
||||
language = args.get('language')
|
||||
forced = args.get('forced')
|
||||
hi = args.get('hi')
|
||||
subtitlesPath = args.get('path')
|
||||
|
||||
subtitlesPath = path_mappings.path_replace_reverse(subtitlesPath)
|
||||
|
||||
|
|
|
@ -5,8 +5,7 @@ import os
|
|||
import operator
|
||||
import pretty
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
from functools import reduce
|
||||
from peewee import fn
|
||||
from datetime import timedelta
|
||||
|
@ -14,16 +13,62 @@ from datetime import timedelta
|
|||
from app.database import get_exclusion_clause, TableEpisodes, TableShows, TableHistory, TableBlacklist
|
||||
from app.config import settings
|
||||
from utilities.path_mappings import path_mappings
|
||||
from api.swaggerui import subtitles_language_model
|
||||
|
||||
from ..utils import authenticate, postprocessEpisode
|
||||
|
||||
api_ns_episodes_history = Namespace('Episodes History', description='List episodes history events')
|
||||
|
||||
|
||||
@api_ns_episodes_history.route('episodes/history')
|
||||
class EpisodesHistory(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('start', type=int, required=False, default=0, help='Paging start integer')
|
||||
get_request_parser.add_argument('length', type=int, required=False, default=-1, help='Paging length integer')
|
||||
get_request_parser.add_argument('episodeid', type=int, required=False, help='Episode ID')
|
||||
|
||||
get_language_model = api_ns_episodes_history.model('subtitles_language_model', subtitles_language_model)
|
||||
|
||||
data_model = api_ns_episodes_history.model('history_episodes_data_model', {
|
||||
'id': fields.Integer(),
|
||||
'seriesTitle': fields.String(),
|
||||
'monitored': fields.Boolean(),
|
||||
'episode_number': fields.String(),
|
||||
'episodeTitle': fields.String(),
|
||||
'timestamp': fields.String(),
|
||||
'subs_id': fields.String(),
|
||||
'description': fields.String(),
|
||||
'sonarrSeriesId': fields.Integer(),
|
||||
'language': fields.Nested(get_language_model),
|
||||
'score': fields.String(),
|
||||
'tags': fields.List(fields.String),
|
||||
'action': fields.Integer(),
|
||||
'video_path': fields.String(),
|
||||
'subtitles_path': fields.String(),
|
||||
'sonarrEpisodeId': fields.Integer(),
|
||||
'provider': fields.String(),
|
||||
'seriesType': fields.String(),
|
||||
'upgradable': fields.Boolean(),
|
||||
'raw_timestamp': fields.Integer(),
|
||||
'parsed_timestamp': fields.String(),
|
||||
'blacklisted': fields.Boolean(),
|
||||
})
|
||||
|
||||
get_response_model = api_ns_episodes_history.model('EpisodeHistoryGetResponse', {
|
||||
'data': fields.Nested(data_model),
|
||||
'total': fields.Integer(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_episodes_history.marshal_with(get_response_model, code=200)
|
||||
@api_ns_episodes_history.response(401, 'Not Authenticated')
|
||||
@api_ns_episodes_history.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
episodeid = request.args.get('episodeid')
|
||||
"""List episodes history events"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
start = args.get('start')
|
||||
length = args.get('length')
|
||||
episodeid = args.get('episodeid')
|
||||
|
||||
upgradable_episodes_not_perfect = []
|
||||
if settings.general.getboolean('upgrade_subs'):
|
||||
|
@ -133,4 +178,4 @@ class EpisodesHistory(Resource):
|
|||
.join(TableEpisodes, on=(TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId))\
|
||||
.where(TableEpisodes.title.is_null(False)).count()
|
||||
|
||||
return jsonify(data=episode_history, total=count)
|
||||
return {'data': episode_history, 'total': count}
|
||||
|
|
|
@ -2,20 +2,54 @@
|
|||
|
||||
import operator
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
from functools import reduce
|
||||
|
||||
from app.database import get_exclusion_clause, TableEpisodes, TableShows
|
||||
from api.swaggerui import subtitles_language_model
|
||||
|
||||
from ..utils import authenticate, postprocessEpisode
|
||||
|
||||
api_ns_episodes_wanted = Namespace('Episodes Wanted', description='List episodes wanted subtitles')
|
||||
|
||||
# GET: Get Wanted Episodes
|
||||
|
||||
@api_ns_episodes_wanted.route('episodes/wanted')
|
||||
class EpisodesWanted(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('start', type=int, required=False, default=0, help='Paging start integer')
|
||||
get_request_parser.add_argument('length', type=int, required=False, default=-1, help='Paging length integer')
|
||||
get_request_parser.add_argument('episodeid[]', type=int, action='append', required=False, default=[],
|
||||
help='Episodes ID to list')
|
||||
|
||||
get_subtitles_language_model = api_ns_episodes_wanted.model('subtitles_language_model', subtitles_language_model)
|
||||
|
||||
data_model = api_ns_episodes_wanted.model('wanted_episodes_data_model', {
|
||||
'seriesTitle': fields.String(),
|
||||
'monitored': fields.Boolean(),
|
||||
'episode_number': fields.String(),
|
||||
'episodeTitle': fields.String(),
|
||||
'missing_subtitles': fields.Nested(get_subtitles_language_model),
|
||||
'sonarrSeriesId': fields.Integer(),
|
||||
'sonarrEpisodeId': fields.Integer(),
|
||||
'sceneName': fields.String(),
|
||||
'tags': fields.List(fields.String),
|
||||
'failedAttempts': fields.String(),
|
||||
'seriesType': fields.String(),
|
||||
})
|
||||
|
||||
get_response_model = api_ns_episodes_wanted.model('EpisodeWantedGetResponse', {
|
||||
'data': fields.Nested(data_model),
|
||||
'total': fields.Integer(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_episodes_wanted.marshal_with(get_response_model, code=200)
|
||||
@api_ns_episodes_wanted.response(401, 'Not Authenticated')
|
||||
@api_ns_episodes_wanted.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
episodeid = request.args.getlist('episodeid[]')
|
||||
"""List episodes wanted subtitles"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
episodeid = args.get('episodeid[]')
|
||||
|
||||
wanted_conditions = [(TableEpisodes.missing_subtitles != '[]')]
|
||||
if len(episodeid) > 0:
|
||||
|
@ -39,8 +73,8 @@ class EpisodesWanted(Resource):
|
|||
.where(wanted_condition)\
|
||||
.dicts()
|
||||
else:
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
start = args.get('start')
|
||||
length = args.get('length')
|
||||
data = TableEpisodes.select(TableShows.title.alias('seriesTitle'),
|
||||
TableEpisodes.monitored,
|
||||
TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'),
|
||||
|
@ -72,4 +106,4 @@ class EpisodesWanted(Resource):
|
|||
.where(reduce(operator.and_, count_conditions))\
|
||||
.count()
|
||||
|
||||
return jsonify(data=data, total=count)
|
||||
return {'data': data, 'total': count}
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
from .files import api_ns_files
|
||||
from .files_sonarr import api_ns_files_sonarr
|
||||
from .files_radarr import api_ns_files_radarr
|
||||
|
||||
from .files import BrowseBazarrFS
|
||||
from .files_sonarr import BrowseSonarrFS
|
||||
from .files_radarr import BrowseRadarrFS
|
||||
|
||||
|
||||
api_bp_files = Blueprint('api_files', __name__)
|
||||
api = Api(api_bp_files)
|
||||
|
||||
api.add_resource(BrowseBazarrFS, '/files')
|
||||
api.add_resource(BrowseSonarrFS, '/files/sonarr')
|
||||
api.add_resource(BrowseRadarrFS, '/files/radarr')
|
||||
api_ns_list_files = [
|
||||
api_ns_files,
|
||||
api_ns_files_radarr,
|
||||
api_ns_files_sonarr,
|
||||
]
|
||||
|
|
|
@ -1,24 +1,40 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
|
||||
from utilities.filesystem import browse_bazarr_filesystem
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_files = Namespace('Files Browser for Bazarr', description='Browse content of file system as seen by Bazarr')
|
||||
|
||||
|
||||
@api_ns_files.route('files')
|
||||
class BrowseBazarrFS(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('path', type=str, default='', help='Path to browse')
|
||||
|
||||
get_response_model = api_ns_files.model('BazarrFileBrowserGetResponse', {
|
||||
'name': fields.String(),
|
||||
'children': fields.Boolean(),
|
||||
'path': fields.String(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_files.marshal_with(get_response_model, code=200)
|
||||
@api_ns_files.response(401, 'Not Authenticated')
|
||||
@api_ns_files.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
path = request.args.get('path') or ''
|
||||
"""List Bazarr file system content"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
path = args.get('path')
|
||||
data = []
|
||||
try:
|
||||
result = browse_bazarr_filesystem(path)
|
||||
if result is None:
|
||||
raise ValueError
|
||||
except Exception:
|
||||
return jsonify([])
|
||||
return []
|
||||
for item in result['directories']:
|
||||
data.append({'name': item['name'], 'children': True, 'path': item['path']})
|
||||
return jsonify(data)
|
||||
return data
|
||||
|
|
|
@ -1,24 +1,41 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
|
||||
from radarr.filesystem import browse_radarr_filesystem
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_files_radarr = Namespace('Files Browser for Radarr', description='Browse content of file system as seen by '
|
||||
'Radarr')
|
||||
|
||||
|
||||
@api_ns_files_radarr.route('files/radarr')
|
||||
class BrowseRadarrFS(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('path', type=str, default='', help='Path to browse')
|
||||
|
||||
get_response_model = api_ns_files_radarr.model('RadarrFileBrowserGetResponse', {
|
||||
'name': fields.String(),
|
||||
'children': fields.Boolean(),
|
||||
'path': fields.String(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_files_radarr.marshal_with(get_response_model, code=200)
|
||||
@api_ns_files_radarr.response(401, 'Not Authenticated')
|
||||
@api_ns_files_radarr.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
path = request.args.get('path') or ''
|
||||
"""List Radarr file system content"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
path = args.get('path')
|
||||
data = []
|
||||
try:
|
||||
result = browse_radarr_filesystem(path)
|
||||
if result is None:
|
||||
raise ValueError
|
||||
except Exception:
|
||||
return jsonify([])
|
||||
return []
|
||||
for item in result['directories']:
|
||||
data.append({'name': item['name'], 'children': True, 'path': item['path']})
|
||||
return jsonify(data)
|
||||
return data
|
||||
|
|
|
@ -1,24 +1,41 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
|
||||
from sonarr.filesystem import browse_sonarr_filesystem
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_files_sonarr = Namespace('Files Browser for Sonarr', description='Browse content of file system as seen by '
|
||||
'Sonarr')
|
||||
|
||||
|
||||
@api_ns_files_sonarr.route('files/sonarr')
|
||||
class BrowseSonarrFS(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('path', type=str, default='', help='Path to browse')
|
||||
|
||||
get_response_model = api_ns_files_sonarr.model('SonarrFileBrowserGetResponse', {
|
||||
'name': fields.String(),
|
||||
'children': fields.Boolean(),
|
||||
'path': fields.String(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_files_sonarr.marshal_with(get_response_model, code=200)
|
||||
@api_ns_files_sonarr.response(401, 'Not Authenticated')
|
||||
@api_ns_files_sonarr.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
path = request.args.get('path') or ''
|
||||
"""List Sonarr file system content"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
path = args.get('path')
|
||||
data = []
|
||||
try:
|
||||
result = browse_sonarr_filesystem(path)
|
||||
if result is None:
|
||||
raise ValueError
|
||||
except Exception:
|
||||
return jsonify([])
|
||||
return []
|
||||
for item in result['directories']:
|
||||
data.append({'name': item['name'], 'children': True, 'path': item['path']})
|
||||
return jsonify(data)
|
||||
return data
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .stats import HistoryStats
|
||||
from .stats import api_ns_history_stats
|
||||
|
||||
|
||||
api_bp_history = Blueprint('api_history', __name__)
|
||||
api = Api(api_bp_history)
|
||||
|
||||
api.add_resource(HistoryStats, '/history/stats')
|
||||
api_ns_list_history = [
|
||||
api_ns_history_stats
|
||||
]
|
||||
|
|
|
@ -5,8 +5,7 @@ import datetime
|
|||
import operator
|
||||
|
||||
from dateutil import rrule
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
from functools import reduce
|
||||
from peewee import fn
|
||||
|
||||
|
@ -14,14 +13,45 @@ from app.database import TableHistory, TableHistoryMovie
|
|||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_history_stats = Namespace('History Statistics', description='Get history statistics')
|
||||
|
||||
|
||||
@api_ns_history_stats.route('history/stats')
|
||||
class HistoryStats(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('timeFrame', type=str, default='month',
|
||||
help='Timeframe to get stats for. Must be in ["week", "month", "trimester", '
|
||||
'"year"]')
|
||||
get_request_parser.add_argument('action', type=str, default='All', help='Action type to filter for.')
|
||||
get_request_parser.add_argument('provider', type=str, default='All', help='Provider name to filter for.')
|
||||
get_request_parser.add_argument('language', type=str, default='All', help='Language name to filter for')
|
||||
|
||||
series_data_model = api_ns_history_stats.model('history_series_stats_data_model', {
|
||||
'date': fields.String(),
|
||||
'count': fields.Integer(),
|
||||
})
|
||||
|
||||
movies_data_model = api_ns_history_stats.model('history_movies_stats_data_model', {
|
||||
'date': fields.String(),
|
||||
'count': fields.Integer(),
|
||||
})
|
||||
|
||||
get_response_model = api_ns_history_stats.model('HistoryStatsGetResponse', {
|
||||
'series': fields.Nested(series_data_model),
|
||||
'movies': fields.Nested(movies_data_model),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_history_stats.marshal_with(get_response_model, code=200)
|
||||
@api_ns_history_stats.response(401, 'Not Authenticated')
|
||||
@api_ns_history_stats.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
timeframe = request.args.get('timeFrame') or 'month'
|
||||
action = request.args.get('action') or 'All'
|
||||
provider = request.args.get('provider') or 'All'
|
||||
language = request.args.get('language') or 'All'
|
||||
"""Get history statistics"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
timeframe = args.get('timeFrame')
|
||||
action = args.get('action')
|
||||
provider = args.get('provider')
|
||||
language = args.get('language')
|
||||
|
||||
# timeframe must be in ['week', 'month', 'trimester', 'year']
|
||||
if timeframe == 'year':
|
||||
|
@ -82,4 +112,4 @@ class HistoryStats(Resource):
|
|||
sorted_data_series = sorted(data_series, key=lambda i: i['date'])
|
||||
sorted_data_movies = sorted(data_movies, key=lambda i: i['date'])
|
||||
|
||||
return jsonify(series=sorted_data_series, movies=sorted_data_movies)
|
||||
return {'series': sorted_data_series, 'movies': sorted_data_movies}
|
||||
|
|
|
@ -1,20 +1,16 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .movies import Movies
|
||||
from .movies_subtitles import MoviesSubtitles
|
||||
from .history import MoviesHistory
|
||||
from .wanted import MoviesWanted
|
||||
from .blacklist import MoviesBlacklist
|
||||
from .movies import api_ns_movies
|
||||
from .movies_subtitles import api_ns_movies_subtitles
|
||||
from .history import api_ns_movies_history
|
||||
from .wanted import api_ns_movies_wanted
|
||||
from .blacklist import api_ns_movies_blacklist
|
||||
|
||||
|
||||
api_bp_movies = Blueprint('api_movies', __name__)
|
||||
api = Api(api_bp_movies)
|
||||
|
||||
api.add_resource(Movies, '/movies')
|
||||
api.add_resource(MoviesWanted, '/movies/wanted')
|
||||
api.add_resource(MoviesSubtitles, '/movies/subtitles')
|
||||
api.add_resource(MoviesHistory, '/movies/history')
|
||||
api.add_resource(MoviesBlacklist, '/movies/blacklist')
|
||||
api_ns_list_movies = [
|
||||
api_ns_movies,
|
||||
api_ns_movies_blacklist,
|
||||
api_ns_movies_history,
|
||||
api_ns_movies_subtitles,
|
||||
api_ns_movies_wanted,
|
||||
]
|
||||
|
|
|
@ -3,8 +3,7 @@
|
|||
import datetime
|
||||
import pretty
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
|
||||
from app.database import TableMovies, TableBlacklistMovie
|
||||
from subtitles.tools.delete import delete_subtitles
|
||||
|
@ -12,18 +11,41 @@ from radarr.blacklist import blacklist_log_movie, blacklist_delete_all_movie, bl
|
|||
from utilities.path_mappings import path_mappings
|
||||
from subtitles.mass_download import movies_download_subtitles
|
||||
from app.event_handler import event_stream
|
||||
from api.swaggerui import subtitles_language_model
|
||||
|
||||
from ..utils import authenticate, postprocessMovie
|
||||
|
||||
api_ns_movies_blacklist = Namespace('Movies Blacklist', description='List, add or remove subtitles to or from '
|
||||
'movies blacklist')
|
||||
|
||||
# GET: get blacklist
|
||||
# POST: add blacklist
|
||||
# DELETE: remove blacklist
|
||||
|
||||
@api_ns_movies_blacklist.route('movies/blacklist')
|
||||
class MoviesBlacklist(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('start', type=int, required=False, default=0, help='Paging start integer')
|
||||
get_request_parser.add_argument('length', type=int, required=False, default=-1, help='Paging length integer')
|
||||
|
||||
get_language_model = api_ns_movies_blacklist.model('subtitles_language_model', subtitles_language_model)
|
||||
|
||||
get_response_model = api_ns_movies_blacklist.model('MovieBlacklistGetResponse', {
|
||||
'title': fields.String(),
|
||||
'radarrId': fields.Integer(),
|
||||
'provider': fields.String(),
|
||||
'subs_id': fields.String(),
|
||||
'language': fields.Nested(get_language_model),
|
||||
'timestamp': fields.String(),
|
||||
'parsed_timestamp': fields.String(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_movies_blacklist.marshal_with(get_response_model, envelope='data', code=200)
|
||||
@api_ns_movies_blacklist.response(401, 'Not Authenticated')
|
||||
@api_ns_movies_blacklist.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
"""List blacklisted movies subtitles"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
start = args.get('start')
|
||||
length = args.get('length')
|
||||
|
||||
data = TableBlacklistMovie.select(TableMovies.title,
|
||||
TableMovies.radarrId,
|
||||
|
@ -45,14 +67,27 @@ class MoviesBlacklist(Resource):
|
|||
item["parsed_timestamp"] = datetime.datetime.fromtimestamp(int(item['timestamp'])).strftime('%x %X')
|
||||
item.update({'timestamp': pretty.date(datetime.datetime.fromtimestamp(item['timestamp']))})
|
||||
|
||||
return jsonify(data=data)
|
||||
return data
|
||||
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('radarrid', type=int, required=True, help='Radarr ID')
|
||||
post_request_parser.add_argument('provider', type=str, required=True, help='Provider name')
|
||||
post_request_parser.add_argument('subs_id', type=str, required=True, help='Subtitles ID')
|
||||
post_request_parser.add_argument('language', type=str, required=True, help='Subtitles language')
|
||||
post_request_parser.add_argument('subtitles_path', type=str, required=True, help='Subtitles file path')
|
||||
|
||||
@authenticate
|
||||
@api_ns_movies_blacklist.doc(parser=post_request_parser)
|
||||
@api_ns_movies_blacklist.response(200, 'Success')
|
||||
@api_ns_movies_blacklist.response(401, 'Not Authenticated')
|
||||
@api_ns_movies_blacklist.response(404, 'Movie not found')
|
||||
def post(self):
|
||||
radarr_id = int(request.args.get('radarrid'))
|
||||
provider = request.form.get('provider')
|
||||
subs_id = request.form.get('subs_id')
|
||||
language = request.form.get('language')
|
||||
"""Add a movies subtitles to blacklist"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
radarr_id = args.get('radarrid')
|
||||
provider = args.get('provider')
|
||||
subs_id = args.get('subs_id')
|
||||
language = args.get('language')
|
||||
# TODO
|
||||
forced = False
|
||||
hi = False
|
||||
|
@ -63,7 +98,7 @@ class MoviesBlacklist(Resource):
|
|||
return 'Movie not found', 404
|
||||
|
||||
media_path = data['path']
|
||||
subtitles_path = request.form.get('subtitles_path')
|
||||
subtitles_path = args.get('subtitles_path')
|
||||
|
||||
blacklist_log_movie(radarr_id=radarr_id,
|
||||
provider=provider,
|
||||
|
@ -80,12 +115,22 @@ class MoviesBlacklist(Resource):
|
|||
event_stream(type='movie-history')
|
||||
return '', 200
|
||||
|
||||
delete_request_parser = reqparse.RequestParser()
|
||||
delete_request_parser.add_argument('all', type=str, required=False, help='Empty movies subtitles blacklist')
|
||||
delete_request_parser.add_argument('provider', type=str, required=True, help='Provider name')
|
||||
delete_request_parser.add_argument('subs_id', type=str, required=True, help='Subtitles ID')
|
||||
|
||||
@authenticate
|
||||
@api_ns_movies_blacklist.doc(parser=delete_request_parser)
|
||||
@api_ns_movies_blacklist.response(204, 'Success')
|
||||
@api_ns_movies_blacklist.response(401, 'Not Authenticated')
|
||||
def delete(self):
|
||||
if request.args.get("all") == "true":
|
||||
"""Delete a movies subtitles from blacklist"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
if args.get("all") == "true":
|
||||
blacklist_delete_all_movie()
|
||||
else:
|
||||
provider = request.form.get('provider')
|
||||
subs_id = request.form.get('subs_id')
|
||||
provider = args.get('provider')
|
||||
subs_id = args.get('subs_id')
|
||||
blacklist_delete_movie(provider=provider, subs_id=subs_id)
|
||||
return '', 200
|
||||
|
|
|
@ -5,8 +5,7 @@ import os
|
|||
import operator
|
||||
import pretty
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
from functools import reduce
|
||||
from peewee import fn
|
||||
from datetime import timedelta
|
||||
|
@ -14,16 +13,58 @@ from datetime import timedelta
|
|||
from app.database import get_exclusion_clause, TableMovies, TableHistoryMovie, TableBlacklistMovie
|
||||
from app.config import settings
|
||||
from utilities.path_mappings import path_mappings
|
||||
from api.swaggerui import subtitles_language_model
|
||||
|
||||
from ..utils import authenticate, postprocessMovie
|
||||
|
||||
api_ns_movies_history = Namespace('Movies History', description='List movies history events')
|
||||
|
||||
|
||||
@api_ns_movies_history.route('movies/history')
|
||||
class MoviesHistory(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('start', type=int, required=False, default=0, help='Paging start integer')
|
||||
get_request_parser.add_argument('length', type=int, required=False, default=-1, help='Paging length integer')
|
||||
get_request_parser.add_argument('radarrid', type=int, required=False, help='Movie ID')
|
||||
|
||||
get_language_model = api_ns_movies_history.model('subtitles_language_model', subtitles_language_model)
|
||||
|
||||
data_model = api_ns_movies_history.model('history_movies_data_model', {
|
||||
'id': fields.Integer(),
|
||||
'action': fields.Integer(),
|
||||
'title': fields.String(),
|
||||
'timestamp': fields.String(),
|
||||
'description': fields.String(),
|
||||
'radarrId': fields.Integer(),
|
||||
'monitored': fields.Boolean(),
|
||||
'path': fields.String(),
|
||||
'language': fields.Nested(get_language_model),
|
||||
'tags': fields.List(fields.String),
|
||||
'score': fields.String(),
|
||||
'subs_id': fields.String(),
|
||||
'provider': fields.String(),
|
||||
'subtitles_path': fields.String(),
|
||||
'upgradable': fields.Boolean(),
|
||||
'raw_timestamp': fields.Integer(),
|
||||
'parsed_timestamp': fields.String(),
|
||||
'blacklisted': fields.Boolean(),
|
||||
})
|
||||
|
||||
get_response_model = api_ns_movies_history.model('MovieHistoryGetResponse', {
|
||||
'data': fields.Nested(data_model),
|
||||
'total': fields.Integer(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_movies_history.marshal_with(get_response_model, code=200)
|
||||
@api_ns_movies_history.response(401, 'Not Authenticated')
|
||||
@api_ns_movies_history.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
radarrid = request.args.get('radarrid')
|
||||
"""List movies history events"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
start = args.get('start')
|
||||
length = args.get('length')
|
||||
radarrid = args.get('radarrid')
|
||||
|
||||
upgradable_movies = []
|
||||
upgradable_movies_not_perfect = []
|
||||
|
@ -129,4 +170,4 @@ class MoviesHistory(Resource):
|
|||
.where(TableMovies.title.is_null(False))\
|
||||
.count()
|
||||
|
||||
return jsonify(data=movie_history, total=count)
|
||||
return {'data': movie_history, 'total': count}
|
||||
|
|
|
@ -1,23 +1,78 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
|
||||
from app.database import TableMovies
|
||||
from subtitles.indexer.movies import list_missing_subtitles_movies, movies_scan_subtitles
|
||||
from app.event_handler import event_stream
|
||||
from subtitles.wanted import wanted_search_missing_subtitles_movies
|
||||
from subtitles.mass_download import movies_download_subtitles
|
||||
from api.swaggerui import subtitles_model, subtitles_language_model, audio_language_model
|
||||
|
||||
from ..utils import authenticate, postprocessMovie, None_Keys
|
||||
|
||||
|
||||
api_ns_movies = Namespace('Movies', description='List movies metadata, update movie languages profile or run actions '
|
||||
'for specific movies.')
|
||||
|
||||
|
||||
@api_ns_movies.route('movies')
|
||||
class Movies(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('start', type=int, required=False, default=0, help='Paging start integer')
|
||||
get_request_parser.add_argument('length', type=int, required=False, default=-1, help='Paging length integer')
|
||||
get_request_parser.add_argument('radarrid[]', type=int, action='append', required=False, default=[],
|
||||
help='Movies IDs to get metadata for')
|
||||
|
||||
get_subtitles_model = api_ns_movies.model('subtitles_model', subtitles_model)
|
||||
get_subtitles_language_model = api_ns_movies.model('subtitles_language_model', subtitles_language_model)
|
||||
get_audio_language_model = api_ns_movies.model('audio_language_model', audio_language_model)
|
||||
|
||||
data_model = api_ns_movies.model('movies_data_model', {
|
||||
'alternativeTitles': fields.List(fields.String),
|
||||
'audio_codec': fields.String(),
|
||||
'audio_language': fields.Nested(get_audio_language_model),
|
||||
'failedAttempts': fields.String(),
|
||||
'fanart': fields.String(),
|
||||
'file_size': fields.Integer(),
|
||||
'format': fields.String(),
|
||||
'imdbId': fields.String(),
|
||||
'missing_subtitles': fields.Nested(get_subtitles_language_model),
|
||||
'monitored': fields.Boolean(),
|
||||
'movie_file_id': fields.Integer(),
|
||||
'overview': fields.String(),
|
||||
'path': fields.String(),
|
||||
'poster': fields.String(),
|
||||
'profileId': fields.Integer(),
|
||||
'radarrId': fields.Integer(),
|
||||
'resolution': fields.String(),
|
||||
'rowid': fields.Integer(),
|
||||
'sceneName': fields.String(),
|
||||
'sortTitle': fields.String(),
|
||||
'subtitles': fields.Nested(get_subtitles_model),
|
||||
'tags': fields.List(fields.String),
|
||||
'title': fields.String(),
|
||||
'tmdbId': fields.String(),
|
||||
'video_codec': fields.String(),
|
||||
'year': fields.String(),
|
||||
})
|
||||
|
||||
get_response_model = api_ns_movies.model('MoviesGetResponse', {
|
||||
'data': fields.Nested(data_model),
|
||||
'total': fields.Integer(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_movies.marshal_with(get_response_model, code=200)
|
||||
@api_ns_movies.doc(parser=get_request_parser)
|
||||
@api_ns_movies.response(200, 'Success')
|
||||
@api_ns_movies.response(401, 'Not Authenticated')
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
radarrId = request.args.getlist('radarrid[]')
|
||||
"""List movies metadata for specific movies"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
start = args.get('start')
|
||||
length = args.get('length')
|
||||
radarrId = args.get('radarrid[]')
|
||||
|
||||
count = TableMovies.select().count()
|
||||
|
||||
|
@ -32,12 +87,24 @@ class Movies(Resource):
|
|||
for item in result:
|
||||
postprocessMovie(item)
|
||||
|
||||
return jsonify(data=result, total=count)
|
||||
return {'data': result, 'total': count}
|
||||
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('radarrid', type=int, action='append', required=False, default=[],
|
||||
help='Radarr movie(s) ID')
|
||||
post_request_parser.add_argument('profileid', type=str, action='append', required=False, default=[],
|
||||
help='Languages profile(s) ID or "none"')
|
||||
|
||||
@authenticate
|
||||
@api_ns_movies.doc(parser=post_request_parser)
|
||||
@api_ns_movies.response(204, 'Success')
|
||||
@api_ns_movies.response(401, 'Not Authenticated')
|
||||
@api_ns_movies.response(404, 'Languages profile not found')
|
||||
def post(self):
|
||||
radarrIdList = request.form.getlist('radarrid')
|
||||
profileIdList = request.form.getlist('profileid')
|
||||
"""Update specific movies languages profile"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
radarrIdList = args.get('radarrid')
|
||||
profileIdList = args.get('profileid')
|
||||
|
||||
for idx in range(len(radarrIdList)):
|
||||
radarrId = radarrIdList[idx]
|
||||
|
@ -65,10 +132,21 @@ class Movies(Resource):
|
|||
|
||||
return '', 204
|
||||
|
||||
patch_request_parser = reqparse.RequestParser()
|
||||
patch_request_parser.add_argument('radarrid', type=int, required=False, help='Radarr movie ID')
|
||||
patch_request_parser.add_argument('action', type=str, required=False, help='Action to perform from ["scan-disk", '
|
||||
'"search-missing", "search-wanted"]')
|
||||
|
||||
@authenticate
|
||||
@api_ns_movies.doc(parser=patch_request_parser)
|
||||
@api_ns_movies.response(204, 'Success')
|
||||
@api_ns_movies.response(400, 'Unknown action')
|
||||
@api_ns_movies.response(401, 'Not Authenticated')
|
||||
def patch(self):
|
||||
radarrid = request.form.get('radarrid')
|
||||
action = request.form.get('action')
|
||||
"""Run actions on specific movies"""
|
||||
args = self.patch_request_parser.parse_args()
|
||||
radarrid = args.get('radarrid')
|
||||
action = args.get('action')
|
||||
if action == "scan-disk":
|
||||
movies_scan_subtitles(radarrid)
|
||||
return '', 204
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
import os
|
||||
import logging
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
from subliminal_patch.core import SUBTITLE_EXTENSIONS
|
||||
from werkzeug.datastructures import FileStorage
|
||||
|
||||
from app.database import TableMovies, get_audio_profile_languages, get_profile_id
|
||||
from utilities.path_mappings import path_mappings
|
||||
|
@ -21,14 +21,26 @@ from app.config import settings
|
|||
from ..utils import authenticate
|
||||
|
||||
|
||||
# PATCH: Download Subtitles
|
||||
# POST: Upload Subtitles
|
||||
# DELETE: Delete Subtitles
|
||||
api_ns_movies_subtitles = Namespace('Movies Subtitles', description='Download, upload or delete movies subtitles')
|
||||
|
||||
|
||||
@api_ns_movies_subtitles.route('movies/subtitles')
|
||||
class MoviesSubtitles(Resource):
|
||||
patch_request_parser = reqparse.RequestParser()
|
||||
patch_request_parser.add_argument('radarrid', type=int, required=True, help='Movie ID')
|
||||
patch_request_parser.add_argument('language', type=str, required=True, help='Language code2')
|
||||
patch_request_parser.add_argument('forced', type=str, required=True, help='Forced true/false as string')
|
||||
patch_request_parser.add_argument('hi', type=str, required=True, help='HI true/false as string')
|
||||
|
||||
@authenticate
|
||||
@api_ns_movies_subtitles.doc(parser=patch_request_parser)
|
||||
@api_ns_movies_subtitles.response(204, 'Success')
|
||||
@api_ns_movies_subtitles.response(401, 'Not Authenticated')
|
||||
@api_ns_movies_subtitles.response(404, 'Movie not found')
|
||||
def patch(self):
|
||||
# Download
|
||||
radarrId = request.args.get('radarrid')
|
||||
"""Download a movie subtitles"""
|
||||
args = self.patch_request_parser.parse_args()
|
||||
radarrId = args.get('radarrid')
|
||||
|
||||
movieInfo = TableMovies.select(TableMovies.title,
|
||||
TableMovies.path,
|
||||
|
@ -47,9 +59,9 @@ class MoviesSubtitles(Resource):
|
|||
title = movieInfo['title']
|
||||
audio_language = movieInfo['audio_language']
|
||||
|
||||
language = request.form.get('language')
|
||||
hi = request.form.get('hi').capitalize()
|
||||
forced = request.form.get('forced').capitalize()
|
||||
language = args.get('language')
|
||||
hi = args.get('hi').capitalize()
|
||||
forced = args.get('forced').capitalize()
|
||||
|
||||
audio_language_list = get_audio_profile_languages(movie_id=radarrId)
|
||||
if len(audio_language_list) > 0:
|
||||
|
@ -85,11 +97,25 @@ class MoviesSubtitles(Resource):
|
|||
|
||||
return '', 204
|
||||
|
||||
# POST: Upload Subtitles
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('radarrid', type=int, required=True, help='Movie ID')
|
||||
post_request_parser.add_argument('language', type=str, required=True, help='Language code2')
|
||||
post_request_parser.add_argument('forced', type=str, required=True, help='Forced true/false as string')
|
||||
post_request_parser.add_argument('hi', type=str, required=True, help='HI true/false as string')
|
||||
post_request_parser.add_argument('file', type=FileStorage, location='files', required=True,
|
||||
help='Subtitles file as file upload object')
|
||||
|
||||
@authenticate
|
||||
@api_ns_movies_subtitles.doc(parser=post_request_parser)
|
||||
@api_ns_movies_subtitles.response(204, 'Success')
|
||||
@api_ns_movies_subtitles.response(401, 'Not Authenticated')
|
||||
@api_ns_movies_subtitles.response(404, 'Movie not found')
|
||||
def post(self):
|
||||
# Upload
|
||||
"""Upload a movie subtitles"""
|
||||
# TODO: Support Multiply Upload
|
||||
radarrId = request.args.get('radarrid')
|
||||
args = self.post_request_parser.parse_args()
|
||||
radarrId = args.get('radarrid')
|
||||
movieInfo = TableMovies.select(TableMovies.title,
|
||||
TableMovies.path,
|
||||
TableMovies.sceneName,
|
||||
|
@ -107,10 +133,10 @@ class MoviesSubtitles(Resource):
|
|||
title = movieInfo['title']
|
||||
audioLanguage = movieInfo['audio_language']
|
||||
|
||||
language = request.form.get('language')
|
||||
forced = True if request.form.get('forced') == 'true' else False
|
||||
hi = True if request.form.get('hi') == 'true' else False
|
||||
subFile = request.files.get('file')
|
||||
language = args.get('language')
|
||||
forced = True if args.get('forced') == 'true' else False
|
||||
hi = True if args.get('hi') == 'true' else False
|
||||
subFile = args.get('file')
|
||||
|
||||
_, ext = os.path.splitext(subFile.filename)
|
||||
|
||||
|
@ -151,10 +177,23 @@ class MoviesSubtitles(Resource):
|
|||
|
||||
return '', 204
|
||||
|
||||
# DELETE: Delete Subtitles
|
||||
delete_request_parser = reqparse.RequestParser()
|
||||
delete_request_parser.add_argument('radarrid', type=int, required=True, help='Movie ID')
|
||||
delete_request_parser.add_argument('language', type=str, required=True, help='Language code2')
|
||||
delete_request_parser.add_argument('forced', type=str, required=True, help='Forced true/false as string')
|
||||
delete_request_parser.add_argument('hi', type=str, required=True, help='HI true/false as string')
|
||||
delete_request_parser.add_argument('path', type=str, required=True, help='Path of the subtitles file')
|
||||
|
||||
@authenticate
|
||||
@api_ns_movies_subtitles.doc(parser=delete_request_parser)
|
||||
@api_ns_movies_subtitles.response(204, 'Success')
|
||||
@api_ns_movies_subtitles.response(401, 'Not Authenticated')
|
||||
@api_ns_movies_subtitles.response(404, 'Movie not found')
|
||||
def delete(self):
|
||||
# Delete
|
||||
radarrId = request.args.get('radarrid')
|
||||
"""Delete a movie subtitles"""
|
||||
args = self.delete_request_parser.parse_args()
|
||||
radarrId = args.get('radarrid')
|
||||
movieInfo = TableMovies.select(TableMovies.path) \
|
||||
.where(TableMovies.radarrId == radarrId) \
|
||||
.dicts() \
|
||||
|
@ -165,21 +204,19 @@ class MoviesSubtitles(Resource):
|
|||
|
||||
moviePath = path_mappings.path_replace_movie(movieInfo['path'])
|
||||
|
||||
language = request.form.get('language')
|
||||
forced = request.form.get('forced')
|
||||
hi = request.form.get('hi')
|
||||
subtitlesPath = request.form.get('path')
|
||||
language = args.get('language')
|
||||
forced = args.get('forced')
|
||||
hi = args.get('hi')
|
||||
subtitlesPath = args.get('path')
|
||||
|
||||
subtitlesPath = path_mappings.path_replace_reverse_movie(subtitlesPath)
|
||||
|
||||
result = delete_subtitles(media_type='movie',
|
||||
language=language,
|
||||
forced=forced,
|
||||
hi=hi,
|
||||
media_path=moviePath,
|
||||
subtitles_path=subtitlesPath,
|
||||
radarr_id=radarrId)
|
||||
if result:
|
||||
return '', 202
|
||||
else:
|
||||
return '', 204
|
||||
delete_subtitles(media_type='movie',
|
||||
language=language,
|
||||
forced=forced,
|
||||
hi=hi,
|
||||
media_path=moviePath,
|
||||
subtitles_path=subtitlesPath,
|
||||
radarr_id=radarrId)
|
||||
|
||||
return '', 204
|
||||
|
|
|
@ -2,20 +2,51 @@
|
|||
|
||||
import operator
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
from functools import reduce
|
||||
|
||||
from app.database import get_exclusion_clause, TableMovies
|
||||
from api.swaggerui import subtitles_language_model
|
||||
|
||||
from ..utils import authenticate, postprocessMovie
|
||||
|
||||
|
||||
# GET: Get Wanted Movies
|
||||
api_ns_movies_wanted = Namespace('Movies Wanted', description='List movies wanted subtitles')
|
||||
|
||||
|
||||
@api_ns_movies_wanted.route('movies/wanted')
|
||||
class MoviesWanted(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('start', type=int, required=False, default=0, help='Paging start integer')
|
||||
get_request_parser.add_argument('length', type=int, required=False, default=-1, help='Paging length integer')
|
||||
get_request_parser.add_argument('radarrid[]', type=int, action='append', required=False, default=[],
|
||||
help='Movies ID to list')
|
||||
|
||||
get_subtitles_language_model = api_ns_movies_wanted.model('subtitles_language_model', subtitles_language_model)
|
||||
|
||||
data_model = api_ns_movies_wanted.model('wanted_movies_data_model', {
|
||||
'title': fields.String(),
|
||||
'monitored': fields.Boolean(),
|
||||
'missing_subtitles': fields.Nested(get_subtitles_language_model),
|
||||
'radarrId': fields.Integer(),
|
||||
'sceneName': fields.String(),
|
||||
'tags': fields.List(fields.String),
|
||||
'failedAttempts': fields.String(),
|
||||
})
|
||||
|
||||
get_response_model = api_ns_movies_wanted.model('MovieWantedGetResponse', {
|
||||
'data': fields.Nested(data_model),
|
||||
'total': fields.Integer(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_movies_wanted.marshal_with(get_response_model, code=200)
|
||||
@api_ns_movies_wanted.response(401, 'Not Authenticated')
|
||||
@api_ns_movies_wanted.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
radarrid = request.args.getlist("radarrid[]")
|
||||
"""List movies wanted subtitles"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
radarrid = args.get("radarrid[]")
|
||||
|
||||
wanted_conditions = [(TableMovies.missing_subtitles != '[]')]
|
||||
if len(radarrid) > 0:
|
||||
|
@ -34,8 +65,8 @@ class MoviesWanted(Resource):
|
|||
.where(wanted_condition)\
|
||||
.dicts()
|
||||
else:
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
start = args.get('start')
|
||||
length = args.get('length')
|
||||
result = TableMovies.select(TableMovies.title,
|
||||
TableMovies.missing_subtitles,
|
||||
TableMovies.radarrId,
|
||||
|
@ -60,4 +91,4 @@ class MoviesWanted(Resource):
|
|||
.where(reduce(operator.and_, count_conditions))\
|
||||
.count()
|
||||
|
||||
return jsonify(data=result, total=count)
|
||||
return {'data': result, 'total': count}
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .providers import Providers
|
||||
from .providers_episodes import ProviderEpisodes
|
||||
from .providers_movies import ProviderMovies
|
||||
from .providers import api_ns_providers
|
||||
from .providers_episodes import api_ns_providers_episodes
|
||||
from .providers_movies import api_ns_providers_movies
|
||||
|
||||
|
||||
api_bp_providers = Blueprint('api_providers', __name__)
|
||||
api = Api(api_bp_providers)
|
||||
|
||||
api.add_resource(Providers, '/providers')
|
||||
api.add_resource(ProviderMovies, '/providers/movies')
|
||||
api.add_resource(ProviderEpisodes, '/providers/episodes')
|
||||
api_ns_list_providers = [
|
||||
api_ns_providers,
|
||||
api_ns_providers_episodes,
|
||||
api_ns_providers_movies,
|
||||
]
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
from operator import itemgetter
|
||||
|
||||
from app.database import TableHistory, TableHistoryMovie
|
||||
|
@ -9,11 +8,29 @@ from app.get_providers import list_throttled_providers, reset_throttled_provider
|
|||
|
||||
from ..utils import authenticate, False_Keys
|
||||
|
||||
api_ns_providers = Namespace('Providers', description='Get and reset providers status')
|
||||
|
||||
|
||||
@api_ns_providers.route('providers')
|
||||
class Providers(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('history', type=str, required=False, help='Provider name for history stats')
|
||||
|
||||
get_response_model = api_ns_providers.model('MovieBlacklistGetResponse', {
|
||||
'name': fields.String(),
|
||||
'status': fields.String(),
|
||||
'retry': fields.String(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_providers.marshal_with(get_response_model, envelope='data', code=200)
|
||||
@api_ns_providers.response(200, 'Success')
|
||||
@api_ns_providers.response(401, 'Not Authenticated')
|
||||
@api_ns_providers.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
history = request.args.get('history')
|
||||
"""Get providers status"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
history = args.get('history')
|
||||
if history and history not in False_Keys:
|
||||
providers = list(TableHistory.select(TableHistory.provider)
|
||||
.where(TableHistory.provider is not None and TableHistory.provider != "manual")
|
||||
|
@ -29,22 +46,30 @@ class Providers(Resource):
|
|||
'status': 'History',
|
||||
'retry': '-'
|
||||
})
|
||||
return jsonify(data=sorted(providers_dicts, key=itemgetter('name')))
|
||||
else:
|
||||
throttled_providers = list_throttled_providers()
|
||||
|
||||
throttled_providers = list_throttled_providers()
|
||||
providers_dicts = list()
|
||||
for provider in throttled_providers:
|
||||
providers_dicts.append({
|
||||
"name": provider[0],
|
||||
"status": provider[1] if provider[1] is not None else "Good",
|
||||
"retry": provider[2] if provider[2] != "now" else "-"
|
||||
})
|
||||
return sorted(providers_dicts, key=itemgetter('name'))
|
||||
|
||||
providers = list()
|
||||
for provider in throttled_providers:
|
||||
providers.append({
|
||||
"name": provider[0],
|
||||
"status": provider[1] if provider[1] is not None else "Good",
|
||||
"retry": provider[2] if provider[2] != "now" else "-"
|
||||
})
|
||||
return jsonify(data=providers)
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('action', type=str, required=True, help='Action to perform from ["reset"]')
|
||||
|
||||
@authenticate
|
||||
@api_ns_providers.doc(parser=post_request_parser)
|
||||
@api_ns_providers.response(204, 'Success')
|
||||
@api_ns_providers.response(401, 'Not Authenticated')
|
||||
@api_ns_providers.response(400, 'Unknown action')
|
||||
def post(self):
|
||||
action = request.form.get('action')
|
||||
"""Reset providers status"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
action = args.get('action')
|
||||
|
||||
if action == 'reset':
|
||||
reset_throttled_providers()
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
|
||||
from app.database import TableEpisodes, TableShows, get_audio_profile_languages, get_profile_id
|
||||
from utilities.path_mappings import path_mappings
|
||||
|
@ -15,11 +14,40 @@ from subtitles.indexer.series import store_subtitles
|
|||
from ..utils import authenticate
|
||||
|
||||
|
||||
api_ns_providers_episodes = Namespace('Providers Episodes', description='List and download episodes subtitles manually')
|
||||
|
||||
|
||||
@api_ns_providers_episodes.route('providers/episodes')
|
||||
class ProviderEpisodes(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('episodeid', type=int, required=True, help='Episode ID')
|
||||
|
||||
get_response_model = api_ns_providers_episodes.model('ProviderEpisodesGetResponse', {
|
||||
'dont_matches': fields.List(fields.String),
|
||||
'forced': fields.String(),
|
||||
'hearing_impaired': fields.String(),
|
||||
'language': fields.String(),
|
||||
'matches': fields.List(fields.String),
|
||||
'original_format': fields.String(),
|
||||
'orig_score': fields.Integer(),
|
||||
'provider': fields.String(),
|
||||
'release_info': fields.List(fields.String),
|
||||
'score': fields.Integer(),
|
||||
'score_without_hash': fields.Integer(),
|
||||
'subtitle': fields.String(),
|
||||
'uploader': fields.String(),
|
||||
'url': fields.String(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_providers_episodes.marshal_with(get_response_model, envelope='data', code=200)
|
||||
@api_ns_providers_episodes.response(401, 'Not Authenticated')
|
||||
@api_ns_providers_episodes.response(404, 'Episode not found')
|
||||
@api_ns_providers_episodes.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
# Manual Search
|
||||
sonarrEpisodeId = request.args.get('episodeid')
|
||||
"""Search manually for an episode subtitles"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
sonarrEpisodeId = args.get('episodeid')
|
||||
episodeInfo = TableEpisodes.select(TableEpisodes.path,
|
||||
TableEpisodes.scene_name,
|
||||
TableShows.title,
|
||||
|
@ -42,13 +70,28 @@ class ProviderEpisodes(Resource):
|
|||
data = manual_search(episodePath, profileId, providers_list, sceneName, title, 'series')
|
||||
if not data:
|
||||
data = []
|
||||
return jsonify(data=data)
|
||||
return data
|
||||
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('seriesid', type=int, required=True, help='Series ID')
|
||||
post_request_parser.add_argument('episodeid', type=int, required=True, help='Episode ID')
|
||||
post_request_parser.add_argument('hi', type=str, required=True, help='HI subtitles from ["True", "False"]')
|
||||
post_request_parser.add_argument('forced', type=str, required=True, help='Forced subtitles from ["True", "False"]')
|
||||
post_request_parser.add_argument('original_format', type=str, required=True,
|
||||
help='Use original subtitles format from ["True", "False"]')
|
||||
post_request_parser.add_argument('provider', type=str, required=True, help='Provider name')
|
||||
post_request_parser.add_argument('subtitle', type=str, required=True, help='Pickled subtitles as return by GET')
|
||||
|
||||
@authenticate
|
||||
@api_ns_providers_episodes.doc(parser=post_request_parser)
|
||||
@api_ns_providers_episodes.response(204, 'Success')
|
||||
@api_ns_providers_episodes.response(401, 'Not Authenticated')
|
||||
@api_ns_providers_episodes.response(404, 'Episode not found')
|
||||
def post(self):
|
||||
# Manual Download
|
||||
sonarrSeriesId = request.args.get('seriesid')
|
||||
sonarrEpisodeId = request.args.get('episodeid')
|
||||
"""Manually download an episode subtitles"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
sonarrSeriesId = args.get('seriesid')
|
||||
sonarrEpisodeId = args.get('episodeid')
|
||||
episodeInfo = TableEpisodes.select(TableEpisodes.path,
|
||||
TableEpisodes.scene_name,
|
||||
TableShows.title) \
|
||||
|
@ -64,11 +107,11 @@ class ProviderEpisodes(Resource):
|
|||
episodePath = path_mappings.path_replace(episodeInfo['path'])
|
||||
sceneName = episodeInfo['scene_name'] or "None"
|
||||
|
||||
hi = request.form.get('hi').capitalize()
|
||||
forced = request.form.get('forced').capitalize()
|
||||
use_original_format = request.form.get('original_format').capitalize()
|
||||
selected_provider = request.form.get('provider')
|
||||
subtitle = request.form.get('subtitle')
|
||||
hi = args.get('hi').capitalize()
|
||||
forced = args.get('forced').capitalize()
|
||||
use_original_format = args.get('original_format').capitalize()
|
||||
selected_provider = args.get('provider')
|
||||
subtitle = args.get('subtitle')
|
||||
|
||||
audio_language_list = get_audio_profile_languages(episode_id=sonarrEpisodeId)
|
||||
if len(audio_language_list) > 0:
|
||||
|
@ -78,7 +121,8 @@ class ProviderEpisodes(Resource):
|
|||
|
||||
try:
|
||||
result = manual_download_subtitle(episodePath, audio_language, hi, forced, subtitle, selected_provider,
|
||||
sceneName, title, 'series', use_original_format, profile_id=get_profile_id(episode_id=sonarrEpisodeId))
|
||||
sceneName, title, 'series', use_original_format,
|
||||
profile_id=get_profile_id(episode_id=sonarrEpisodeId))
|
||||
if result is not None:
|
||||
message = result[0]
|
||||
path = result[1]
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
# coding=utf-8
|
||||
|
||||
import logging
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
|
||||
from app.database import TableMovies, get_audio_profile_languages, get_profile_id
|
||||
from utilities.path_mappings import path_mappings
|
||||
|
@ -17,11 +14,40 @@ from subtitles.indexer.movies import store_subtitles_movie
|
|||
from ..utils import authenticate
|
||||
|
||||
|
||||
api_ns_providers_movies = Namespace('Providers Movies', description='List and download movies subtitles manually')
|
||||
|
||||
|
||||
@api_ns_providers_movies.route('providers/movies')
|
||||
class ProviderMovies(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('radarrid', type=int, required=True, help='Movie ID')
|
||||
|
||||
get_response_model = api_ns_providers_movies.model('ProviderMoviesGetResponse', {
|
||||
'dont_matches': fields.List(fields.String),
|
||||
'forced': fields.String(),
|
||||
'hearing_impaired': fields.String(),
|
||||
'language': fields.String(),
|
||||
'matches': fields.List(fields.String),
|
||||
'original_format': fields.String(),
|
||||
'orig_score': fields.Integer(),
|
||||
'provider': fields.String(),
|
||||
'release_info': fields.List(fields.String),
|
||||
'score': fields.Integer(),
|
||||
'score_without_hash': fields.Integer(),
|
||||
'subtitle': fields.String(),
|
||||
'uploader': fields.String(),
|
||||
'url': fields.String(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_providers_movies.marshal_with(get_response_model, envelope='data', code=200)
|
||||
@api_ns_providers_movies.response(401, 'Not Authenticated')
|
||||
@api_ns_providers_movies.response(404, 'Movie not found')
|
||||
@api_ns_providers_movies.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
# Manual Search
|
||||
radarrId = request.args.get('radarrid')
|
||||
"""Search manually for a movie subtitles"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
radarrId = args.get('radarrid')
|
||||
movieInfo = TableMovies.select(TableMovies.title,
|
||||
TableMovies.path,
|
||||
TableMovies.sceneName,
|
||||
|
@ -43,12 +69,26 @@ class ProviderMovies(Resource):
|
|||
data = manual_search(moviePath, profileId, providers_list, sceneName, title, 'movie')
|
||||
if not data:
|
||||
data = []
|
||||
return jsonify(data=data)
|
||||
return data
|
||||
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('radarrid', type=int, required=True, help='Movie ID')
|
||||
post_request_parser.add_argument('hi', type=str, required=True, help='HI subtitles from ["True", "False"]')
|
||||
post_request_parser.add_argument('forced', type=str, required=True, help='Forced subtitles from ["True", "False"]')
|
||||
post_request_parser.add_argument('original_format', type=str, required=True,
|
||||
help='Use original subtitles format from ["True", "False"]')
|
||||
post_request_parser.add_argument('provider', type=str, required=True, help='Provider name')
|
||||
post_request_parser.add_argument('subtitle', type=str, required=True, help='Pickled subtitles as return by GET')
|
||||
|
||||
@authenticate
|
||||
@api_ns_providers_movies.doc(parser=post_request_parser)
|
||||
@api_ns_providers_movies.response(204, 'Success')
|
||||
@api_ns_providers_movies.response(401, 'Not Authenticated')
|
||||
@api_ns_providers_movies.response(404, 'Movie not found')
|
||||
def post(self):
|
||||
# Manual Download
|
||||
radarrId = request.args.get('radarrid')
|
||||
"""Manually download a movie subtitles"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
radarrId = args.get('radarrid')
|
||||
movieInfo = TableMovies.select(TableMovies.title,
|
||||
TableMovies.path,
|
||||
TableMovies.sceneName,
|
||||
|
@ -64,12 +104,11 @@ class ProviderMovies(Resource):
|
|||
moviePath = path_mappings.path_replace_movie(movieInfo['path'])
|
||||
sceneName = movieInfo['sceneName'] or "None"
|
||||
|
||||
hi = request.form.get('hi').capitalize()
|
||||
forced = request.form.get('forced').capitalize()
|
||||
use_original_format = request.form.get('original_format').capitalize()
|
||||
logging.debug(f"use_original_format {use_original_format}")
|
||||
selected_provider = request.form.get('provider')
|
||||
subtitle = request.form.get('subtitle')
|
||||
hi = args.get('hi').capitalize()
|
||||
forced = args.get('forced').capitalize()
|
||||
use_original_format = args.get('original_format').capitalize()
|
||||
selected_provider = args.get('provider')
|
||||
subtitle = args.get('subtitle')
|
||||
|
||||
audio_language_list = get_audio_profile_languages(movie_id=radarrId)
|
||||
if len(audio_language_list) > 0:
|
||||
|
@ -79,7 +118,8 @@ class ProviderMovies(Resource):
|
|||
|
||||
try:
|
||||
result = manual_download_subtitle(moviePath, audio_language, hi, forced, subtitle, selected_provider,
|
||||
sceneName, title, 'movie', use_original_format, profile_id=get_profile_id(movie_id=radarrId))
|
||||
sceneName, title, 'movie', use_original_format,
|
||||
profile_id=get_profile_id(movie_id=radarrId))
|
||||
if result is not None:
|
||||
message = result[0]
|
||||
path = result[1]
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .series import Series
|
||||
from .series import api_ns_series
|
||||
|
||||
|
||||
api_bp_series = Blueprint('api_series', __name__)
|
||||
api = Api(api_bp_series)
|
||||
|
||||
api.add_resource(Series, '/series')
|
||||
api_ns_list_series = [
|
||||
api_ns_series,
|
||||
]
|
||||
|
|
|
@ -2,8 +2,7 @@
|
|||
|
||||
import operator
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
from functools import reduce
|
||||
|
||||
from app.database import get_exclusion_clause, TableEpisodes, TableShows
|
||||
|
@ -11,16 +10,64 @@ from subtitles.indexer.series import list_missing_subtitles, series_scan_subtitl
|
|||
from subtitles.mass_download import series_download_subtitles
|
||||
from subtitles.wanted import wanted_search_missing_subtitles_series
|
||||
from app.event_handler import event_stream
|
||||
from api.swaggerui import subtitles_model, subtitles_language_model, audio_language_model
|
||||
|
||||
from ..utils import authenticate, postprocessSeries, None_Keys
|
||||
|
||||
|
||||
api_ns_series = Namespace('Series', description='List series metadata, update series languages profile or run actions '
|
||||
'for specific series.')
|
||||
|
||||
|
||||
@api_ns_series.route('series')
|
||||
class Series(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('start', type=int, required=False, default=0, help='Paging start integer')
|
||||
get_request_parser.add_argument('length', type=int, required=False, default=-1, help='Paging length integer')
|
||||
get_request_parser.add_argument('seriesid[]', type=int, action='append', required=False, default=[],
|
||||
help='Series IDs to get metadata for')
|
||||
|
||||
get_subtitles_model = api_ns_series.model('subtitles_model', subtitles_model)
|
||||
get_subtitles_language_model = api_ns_series.model('subtitles_language_model', subtitles_language_model)
|
||||
get_audio_language_model = api_ns_series.model('audio_language_model', audio_language_model)
|
||||
|
||||
data_model = api_ns_series.model('series_data_model', {
|
||||
'alternativeTitles': fields.List(fields.String),
|
||||
'audio_language': fields.Nested(get_audio_language_model),
|
||||
'episodeFileCount': fields.Integer(),
|
||||
'episodeMissingCount': fields.Integer(),
|
||||
'fanart': fields.String(),
|
||||
'imdbId': fields.String(),
|
||||
'monitored': fields.Boolean(),
|
||||
'overview': fields.String(),
|
||||
'path': fields.String(),
|
||||
'poster': fields.String(),
|
||||
'profileId': fields.Integer(),
|
||||
'seriesType': fields.String(),
|
||||
'sonarrSeriesId': fields.Integer(),
|
||||
'sortTitle': fields.String(),
|
||||
'tags': fields.List(fields.String),
|
||||
'title': fields.String(),
|
||||
'tvdbId': fields.Integer(),
|
||||
'year': fields.String(),
|
||||
})
|
||||
|
||||
get_response_model = api_ns_series.model('SeriesGetResponse', {
|
||||
'data': fields.Nested(data_model),
|
||||
'total': fields.Integer(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_series.marshal_with(get_response_model, code=200)
|
||||
@api_ns_series.doc(parser=get_request_parser)
|
||||
@api_ns_series.response(200, 'Success')
|
||||
@api_ns_series.response(401, 'Not Authenticated')
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
seriesId = request.args.getlist('seriesid[]')
|
||||
"""List series metadata for specific series"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
start = args.get('start')
|
||||
length = args.get('length')
|
||||
seriesId = args.get('seriesid[]')
|
||||
|
||||
count = TableShows.select().count()
|
||||
|
||||
|
@ -58,12 +105,24 @@ class Series(Resource):
|
|||
.count()
|
||||
item.update({"episodeFileCount": episodeFileCount})
|
||||
|
||||
return jsonify(data=result, total=count)
|
||||
return {'data': result, 'total': count}
|
||||
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('seriesid', type=int, action='append', required=False, default=[],
|
||||
help='Sonarr series ID')
|
||||
post_request_parser.add_argument('profileid', type=str, action='append', required=False, default=[],
|
||||
help='Languages profile(s) ID or "none"')
|
||||
|
||||
@authenticate
|
||||
@api_ns_series.doc(parser=post_request_parser)
|
||||
@api_ns_series.response(204, 'Success')
|
||||
@api_ns_series.response(401, 'Not Authenticated')
|
||||
@api_ns_series.response(404, 'Languages profile not found')
|
||||
def post(self):
|
||||
seriesIdList = request.form.getlist('seriesid')
|
||||
profileIdList = request.form.getlist('profileid')
|
||||
"""Update specific series languages profile"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
seriesIdList = args.get('seriesid')
|
||||
profileIdList = args.get('profileid')
|
||||
|
||||
for idx in range(len(seriesIdList)):
|
||||
seriesId = seriesIdList[idx]
|
||||
|
@ -99,10 +158,21 @@ class Series(Resource):
|
|||
|
||||
return '', 204
|
||||
|
||||
patch_request_parser = reqparse.RequestParser()
|
||||
patch_request_parser.add_argument('seriesid', type=int, required=False, help='Sonarr series ID')
|
||||
patch_request_parser.add_argument('action', type=str, required=False, help='Action to perform from ["scan-disk", '
|
||||
'"search-missing", "search-wanted"]')
|
||||
|
||||
@authenticate
|
||||
@api_ns_series.doc(parser=patch_request_parser)
|
||||
@api_ns_series.response(204, 'Success')
|
||||
@api_ns_series.response(400, 'Unknown action')
|
||||
@api_ns_series.response(401, 'Not Authenticated')
|
||||
def patch(self):
|
||||
seriesid = request.form.get('seriesid')
|
||||
action = request.form.get('action')
|
||||
"""Run actions on specific series"""
|
||||
args = self.patch_request_parser.parse_args()
|
||||
seriesid = args.get('seriesid')
|
||||
action = args.get('action')
|
||||
if action == "scan-disk":
|
||||
series_scan_subtitles(seriesid)
|
||||
return '', 204
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .subtitles import Subtitles
|
||||
from .subtitles_info import SubtitleNameInfo
|
||||
from .subtitles import api_ns_subtitles
|
||||
from .subtitles_info import api_ns_subtitles_info
|
||||
|
||||
|
||||
api_bp_subtitles = Blueprint('api_subtitles', __name__)
|
||||
api = Api(api_bp_subtitles)
|
||||
|
||||
api.add_resource(Subtitles, '/subtitles')
|
||||
api.add_resource(SubtitleNameInfo, '/subtitles/info')
|
||||
api_ns_list_subtitles = [
|
||||
api_ns_subtitles,
|
||||
api_ns_subtitles_info,
|
||||
]
|
||||
|
|
|
@ -4,8 +4,7 @@ import os
|
|||
import sys
|
||||
import gc
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
|
||||
from app.database import TableEpisodes, TableMovies
|
||||
from utilities.path_mappings import path_mappings
|
||||
|
@ -20,15 +19,37 @@ from app.event_handler import event_stream
|
|||
from ..utils import authenticate
|
||||
|
||||
|
||||
class Subtitles(Resource):
|
||||
@authenticate
|
||||
def patch(self):
|
||||
action = request.args.get('action')
|
||||
api_ns_subtitles = Namespace('Subtitles', description='Apply mods/tools on external subtitles')
|
||||
|
||||
language = request.form.get('language')
|
||||
subtitles_path = request.form.get('path')
|
||||
media_type = request.form.get('type')
|
||||
id = request.form.get('id')
|
||||
|
||||
@api_ns_subtitles.route('subtitles')
|
||||
class Subtitles(Resource):
|
||||
patch_request_parser = reqparse.RequestParser()
|
||||
patch_request_parser.add_argument('action', type=str, required=True,
|
||||
help='Action from ["sync", "translate" or mods name]')
|
||||
patch_request_parser.add_argument('language', type=str, required=True, help='Language code2')
|
||||
patch_request_parser.add_argument('path', type=str, required=True, help='Subtitles file path')
|
||||
patch_request_parser.add_argument('type', type=str, required=True, help='Media type from ["episode", "movie"]')
|
||||
patch_request_parser.add_argument('id', type=int, required=True, help='Episode ID')
|
||||
patch_request_parser.add_argument('forced', type=str, required=False, help='Forced subtitles from ["True", "False"]')
|
||||
patch_request_parser.add_argument('hi', type=str, required=False, help='HI subtitles from ["True", "False"]')
|
||||
patch_request_parser.add_argument('original_format', type=str, required=False,
|
||||
help='Use original subtitles format from ["True", "False"]')
|
||||
|
||||
@authenticate
|
||||
@api_ns_subtitles.doc(parser=patch_request_parser)
|
||||
@api_ns_subtitles.response(204, 'Success')
|
||||
@api_ns_subtitles.response(401, 'Not Authenticated')
|
||||
@api_ns_subtitles.response(404, 'Episode/movie not found')
|
||||
def patch(self):
|
||||
"""Apply mods/tools on external subtitles"""
|
||||
args = self.patch_request_parser.parse_args()
|
||||
action = args.get('action')
|
||||
|
||||
language = args.get('language')
|
||||
subtitles_path = args.get('path')
|
||||
media_type = args.get('type')
|
||||
id = args.get('id')
|
||||
|
||||
if media_type == 'episode':
|
||||
metadata = TableEpisodes.select(TableEpisodes.path, TableEpisodes.sonarrSeriesId)\
|
||||
|
@ -62,8 +83,8 @@ class Subtitles(Resource):
|
|||
elif action == 'translate':
|
||||
from_language = os.path.splitext(subtitles_path)[0].rsplit(".", 1)[1].replace('_', '-')
|
||||
dest_language = language
|
||||
forced = True if request.form.get('forced') == 'true' else False
|
||||
hi = True if request.form.get('hi') == 'true' else False
|
||||
forced = True if args.get('forced') == 'true' else False
|
||||
hi = True if args.get('hi') == 'true' else False
|
||||
translate_subtitles_file(video_path=video_path, source_srt_file=subtitles_path,
|
||||
from_lang=from_language, to_lang=dest_language, forced=forced, hi=hi,
|
||||
media_type="series" if media_type == "episode" else "movies",
|
||||
|
@ -71,7 +92,7 @@ class Subtitles(Resource):
|
|||
sonarr_episode_id=int(id),
|
||||
radarr_id=id)
|
||||
else:
|
||||
use_original_format = True if request.form.get('original_format') == 'true' else False
|
||||
use_original_format = True if args.get('original_format') == 'true' else False
|
||||
subtitles_apply_mods(language, subtitles_path, [action], use_original_format)
|
||||
|
||||
# apply chmod if required
|
||||
|
|
|
@ -1,16 +1,37 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
from subliminal_patch.core import guessit
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
api_ns_subtitles_info = Namespace('Subtitles Info', description='Guess season number, episode number or language from '
|
||||
'uploaded subtitles filename')
|
||||
|
||||
|
||||
@api_ns_subtitles_info.route('subtitles/info')
|
||||
class SubtitleNameInfo(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('filenames[]', type=str, required=True, action='append',
|
||||
help='Subtitles filenames')
|
||||
|
||||
get_response_model = api_ns_subtitles_info.model('SubtitlesInfoGetResponse', {
|
||||
'filename': fields.String(),
|
||||
'subtitle_language': fields.String(),
|
||||
'season': fields.Integer(),
|
||||
'episode': fields.Integer(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_subtitles_info.marshal_with(get_response_model, envelope='data', code=200)
|
||||
@api_ns_subtitles_info.response(200, 'Success')
|
||||
@api_ns_subtitles_info.response(401, 'Not Authenticated')
|
||||
@api_ns_subtitles_info.doc(parser=get_request_parser)
|
||||
def get(self):
|
||||
names = request.args.getlist('filenames[]')
|
||||
"""Guessit over subtitles filename"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
names = args.get('filenames[]')
|
||||
results = []
|
||||
for name in names:
|
||||
opts = dict()
|
||||
|
@ -27,16 +48,16 @@ class SubtitleNameInfo(Resource):
|
|||
# for multiple episodes file, choose the first episode number
|
||||
if len(guessit_result['episode']):
|
||||
# make sure that guessit returned a list of more than 0 items
|
||||
result['episode'] = int(guessit_result['episode'][0])
|
||||
elif isinstance(guessit_result['episode'], (str, int)):
|
||||
# if single episode (should be int but just in case we cast it to int)
|
||||
result['episode'] = int(guessit_result['episode'])
|
||||
result['episode'] = guessit_result['episode'][0]
|
||||
elif isinstance(guessit_result['episode'], int):
|
||||
# if single episode
|
||||
result['episode'] = guessit_result['episode']
|
||||
|
||||
if 'season' in guessit_result:
|
||||
result['season'] = int(guessit_result['season'])
|
||||
result['season'] = guessit_result['season']
|
||||
else:
|
||||
result['season'] = 0
|
||||
|
||||
results.append(result)
|
||||
|
||||
return jsonify(data=results)
|
||||
return results
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
# coding=utf-8
|
||||
|
||||
import os
|
||||
|
||||
from flask_restx import fields
|
||||
|
||||
swaggerui_api_params = {"version": os.environ["BAZARR_VERSION"],
|
||||
"description": "API docs for Bazarr",
|
||||
"title": "Bazarr",
|
||||
}
|
||||
|
||||
subtitles_model = {
|
||||
"name": fields.String(),
|
||||
"code2": fields.String(),
|
||||
"code3": fields.String(),
|
||||
"path": fields.String(),
|
||||
"forced": fields.Boolean(),
|
||||
"hi": fields.Boolean()
|
||||
}
|
||||
|
||||
subtitles_language_model = {
|
||||
"name": fields.String(),
|
||||
"code2": fields.String(),
|
||||
"code3": fields.String(),
|
||||
"forced": fields.Boolean(),
|
||||
"hi": fields.Boolean()
|
||||
}
|
||||
|
||||
audio_language_model = {
|
||||
"name": fields.String(),
|
||||
"code2": fields.String(),
|
||||
"code3": fields.String()
|
||||
}
|
|
@ -1,35 +1,31 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
from .system import api_ns_system
|
||||
from .searches import api_ns_system_searches
|
||||
from .account import api_ns_system_account
|
||||
from .backups import api_ns_system_backups
|
||||
from .tasks import api_ns_system_tasks
|
||||
from .logs import api_ns_system_logs
|
||||
from .status import api_ns_system_status
|
||||
from .health import api_ns_system_health
|
||||
from .releases import api_ns_system_releases
|
||||
from .settings import api_ns_system_settings
|
||||
from .languages import api_ns_system_languages
|
||||
from .languages_profiles import api_ns_system_languages_profiles
|
||||
from .notifications import api_ns_system_notifications
|
||||
|
||||
from .system import System
|
||||
from .searches import Searches
|
||||
from .account import SystemAccount
|
||||
from .backups import SystemBackups
|
||||
from .tasks import SystemTasks
|
||||
from .logs import SystemLogs
|
||||
from .status import SystemStatus
|
||||
from .health import SystemHealth
|
||||
from .releases import SystemReleases
|
||||
from .settings import SystemSettings
|
||||
from .languages import Languages
|
||||
from .languages_profiles import LanguagesProfiles
|
||||
from .notifications import Notifications
|
||||
|
||||
api_bp_system = Blueprint('api_system', __name__)
|
||||
api = Api(api_bp_system)
|
||||
|
||||
api.add_resource(System, '/system')
|
||||
api.add_resource(Searches, '/system/searches')
|
||||
api.add_resource(SystemAccount, '/system/account')
|
||||
api.add_resource(SystemBackups, '/system/backups')
|
||||
api.add_resource(SystemTasks, '/system/tasks')
|
||||
api.add_resource(SystemLogs, '/system/logs')
|
||||
api.add_resource(SystemStatus, '/system/status')
|
||||
api.add_resource(SystemHealth, '/system/health')
|
||||
api.add_resource(SystemReleases, '/system/releases')
|
||||
api.add_resource(SystemSettings, '/system/settings')
|
||||
api.add_resource(Languages, '/system/languages')
|
||||
api.add_resource(LanguagesProfiles, '/system/languages/profiles')
|
||||
api.add_resource(Notifications, '/system/notifications')
|
||||
api_ns_list_system = [
|
||||
api_ns_system,
|
||||
api_ns_system_account,
|
||||
api_ns_system_backups,
|
||||
api_ns_system_health,
|
||||
api_ns_system_languages,
|
||||
api_ns_system_languages_profiles,
|
||||
api_ns_system_logs,
|
||||
api_ns_system_notifications,
|
||||
api_ns_system_releases,
|
||||
api_ns_system_searches,
|
||||
api_ns_system_settings,
|
||||
api_ns_system_status,
|
||||
api_ns_system_tasks,
|
||||
]
|
||||
|
|
|
@ -2,22 +2,37 @@
|
|||
|
||||
import gc
|
||||
|
||||
from flask import request, session
|
||||
from flask_restful import Resource
|
||||
from flask import session
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
|
||||
from app.config import settings
|
||||
from utilities.helper import check_credentials
|
||||
|
||||
api_ns_system_account = Namespace('System Account', description='Login or logout from Bazarr UI')
|
||||
|
||||
|
||||
@api_ns_system_account.hide
|
||||
@api_ns_system_account.route('system/account')
|
||||
class SystemAccount(Resource):
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('action', type=str, required=True, help='Action from ["login", "logout"]')
|
||||
post_request_parser.add_argument('username', type=str, required=False, help='Bazarr username')
|
||||
post_request_parser.add_argument('password', type=str, required=False, help='Bazarr password')
|
||||
|
||||
@api_ns_system_account.doc(parser=post_request_parser)
|
||||
@api_ns_system_account.response(204, 'Success')
|
||||
@api_ns_system_account.response(400, 'Unknown action')
|
||||
@api_ns_system_account.response(404, 'Unknown authentication type define in config.ini')
|
||||
def post(self):
|
||||
"""Login or logout from Bazarr UI when using form login"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
if settings.auth.type != 'form':
|
||||
return 'Unknown authentication type define in config.ini', 404
|
||||
|
||||
action = request.args.get('action')
|
||||
action = args.get('action')
|
||||
if action == 'login':
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
username = args.get('username')
|
||||
password = args.get('password')
|
||||
if check_credentials(username, password):
|
||||
session['logged_in'] = True
|
||||
return '', 204
|
||||
|
|
|
@ -1,36 +1,72 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import jsonify, request
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
|
||||
from utilities.backup import get_backup_files, prepare_restore, delete_backup_file, backup_to_zip
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_system_backups = Namespace('System Backups', description='List, create, restore or delete backups')
|
||||
|
||||
|
||||
@api_ns_system_backups.route('system/backups')
|
||||
class SystemBackups(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
backups = get_backup_files(fullpath=False)
|
||||
return jsonify(data=backups)
|
||||
get_response_model = api_ns_system_backups.model('SystemBackupsGetResponse', {
|
||||
'date': fields.String(),
|
||||
'filename': fields.String(),
|
||||
'size': fields.String(),
|
||||
'type': fields.String(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_system_backups.marshal_with(get_response_model, envelope='data', code=200)
|
||||
@api_ns_system_backups.doc(parser=None)
|
||||
@api_ns_system_backups.response(204, 'Success')
|
||||
@api_ns_system_backups.response(401, 'Not Authenticated')
|
||||
def get(self):
|
||||
"""List backup files"""
|
||||
backups = get_backup_files(fullpath=False)
|
||||
return backups
|
||||
|
||||
@authenticate
|
||||
@api_ns_system_backups.doc(parser=None)
|
||||
@api_ns_system_backups.response(204, 'Success')
|
||||
@api_ns_system_backups.response(401, 'Not Authenticated')
|
||||
def post(self):
|
||||
"""Create a new backup"""
|
||||
backup_to_zip()
|
||||
return '', 204
|
||||
|
||||
patch_request_parser = reqparse.RequestParser()
|
||||
patch_request_parser.add_argument('filename', type=str, required=True, help='Backups to restore filename')
|
||||
|
||||
@authenticate
|
||||
@api_ns_system_backups.doc(parser=patch_request_parser)
|
||||
@api_ns_system_backups.response(204, 'Success')
|
||||
@api_ns_system_backups.response(400, 'Filename not provided')
|
||||
@api_ns_system_backups.response(401, 'Not Authenticated')
|
||||
def patch(self):
|
||||
filename = request.form.get('filename')
|
||||
"""Restore a backup file"""
|
||||
args = self.patch_request_parser.parse_args()
|
||||
filename = args.get('filename')
|
||||
if filename:
|
||||
restored = prepare_restore(filename)
|
||||
if restored:
|
||||
return '', 204
|
||||
return 'Filename not provided', 400
|
||||
|
||||
delete_request_parser = reqparse.RequestParser()
|
||||
delete_request_parser.add_argument('filename', type=str, required=True, help='Backups to delete filename')
|
||||
|
||||
@authenticate
|
||||
@api_ns_system_backups.doc(parser=delete_request_parser)
|
||||
@api_ns_system_backups.response(204, 'Success')
|
||||
@api_ns_system_backups.response(400, 'Filename not provided')
|
||||
@api_ns_system_backups.response(401, 'Not Authenticated')
|
||||
def delete(self):
|
||||
filename = request.form.get('filename')
|
||||
"""Delete a backup file"""
|
||||
args = self.delete_request_parser.parse_args()
|
||||
filename = args.get('filename')
|
||||
if filename:
|
||||
deleted = delete_backup_file(filename)
|
||||
if deleted:
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace
|
||||
|
||||
from utilities.health import get_health_issues
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_system_health = Namespace('System Health', description='List health issues')
|
||||
|
||||
|
||||
@api_ns_system_health.route('system/health')
|
||||
class SystemHealth(Resource):
|
||||
@authenticate
|
||||
@api_ns_system_health.doc(parser=None)
|
||||
@api_ns_system_health.response(200, 'Success')
|
||||
@api_ns_system_health.response(401, 'Not Authenticated')
|
||||
def get(self):
|
||||
return jsonify(data=get_health_issues())
|
||||
"""List health issues"""
|
||||
return {'data': get_health_issues()}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
from operator import itemgetter
|
||||
|
||||
from app.database import TableHistory, TableHistoryMovie, TableSettingsLanguages
|
||||
|
@ -9,11 +8,22 @@ from languages.get_languages import alpha2_from_alpha3, language_from_alpha2
|
|||
|
||||
from ..utils import authenticate, False_Keys
|
||||
|
||||
api_ns_system_languages = Namespace('System Languages', description='Get languages list')
|
||||
|
||||
|
||||
@api_ns_system_languages.route('system/languages')
|
||||
class Languages(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('history', type=str, required=False, help='Language name for history stats')
|
||||
|
||||
@authenticate
|
||||
@api_ns_system_languages.doc(parser=get_request_parser)
|
||||
@api_ns_system_languages.response(200, 'Success')
|
||||
@api_ns_system_languages.response(401, 'Not Authenticated')
|
||||
def get(self):
|
||||
history = request.args.get('history')
|
||||
"""List languages for history filter or for language filter menu"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
history = args.get('history')
|
||||
if history and history not in False_Keys:
|
||||
languages = list(TableHistory.select(TableHistory.language)
|
||||
.where(TableHistory.language.is_null(False))
|
||||
|
@ -42,13 +52,13 @@ class Languages(Resource):
|
|||
})
|
||||
except Exception:
|
||||
continue
|
||||
return jsonify(sorted(languages_dicts, key=itemgetter('name')))
|
||||
else:
|
||||
languages_dicts = TableSettingsLanguages.select(TableSettingsLanguages.name,
|
||||
TableSettingsLanguages.code2,
|
||||
TableSettingsLanguages.enabled)\
|
||||
.order_by(TableSettingsLanguages.name).dicts()
|
||||
languages_dicts = list(languages_dicts)
|
||||
for item in languages_dicts:
|
||||
item['enabled'] = item['enabled'] == 1
|
||||
|
||||
result = TableSettingsLanguages.select(TableSettingsLanguages.name,
|
||||
TableSettingsLanguages.code2,
|
||||
TableSettingsLanguages.enabled)\
|
||||
.order_by(TableSettingsLanguages.name).dicts()
|
||||
result = list(result)
|
||||
for item in result:
|
||||
item['enabled'] = item['enabled'] == 1
|
||||
return jsonify(result)
|
||||
return sorted(languages_dicts, key=itemgetter('name'))
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace
|
||||
|
||||
from app.database import get_profiles_list
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_system_languages_profiles = Namespace('System Languages Profiles', description='List languages profiles')
|
||||
|
||||
|
||||
@api_ns_system_languages_profiles.route('system/languages/profiles')
|
||||
class LanguagesProfiles(Resource):
|
||||
@authenticate
|
||||
@api_ns_system_languages_profiles.doc(parser=None)
|
||||
@api_ns_system_languages_profiles.response(200, 'Success')
|
||||
@api_ns_system_languages_profiles.response(401, 'Not Authenticated')
|
||||
def get(self):
|
||||
return jsonify(get_profiles_list())
|
||||
"""List languages profiles"""
|
||||
return get_profiles_list()
|
||||
|
|
|
@ -3,18 +3,32 @@
|
|||
import io
|
||||
import os
|
||||
|
||||
from flask import jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, fields
|
||||
|
||||
from app.logger import empty_log
|
||||
from app.get_args import args
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_system_logs = Namespace('System Logs', description='List log file entries or empty log file')
|
||||
|
||||
|
||||
@api_ns_system_logs.route('system/logs')
|
||||
class SystemLogs(Resource):
|
||||
get_response_model = api_ns_system_logs.model('SystemBackupsGetResponse', {
|
||||
'timestamp': fields.String(),
|
||||
'type': fields.String(),
|
||||
'message': fields.String(),
|
||||
'exception': fields.String(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_system_logs.marshal_with(get_response_model, envelope='data', code=200)
|
||||
@api_ns_system_logs.doc(parser=None)
|
||||
@api_ns_system_logs.response(200, 'Success')
|
||||
@api_ns_system_logs.response(401, 'Not Authenticated')
|
||||
def get(self):
|
||||
"""List log entries"""
|
||||
logs = []
|
||||
with io.open(os.path.join(args.config_dir, 'log', 'bazarr.log'), encoding='UTF-8') as file:
|
||||
raw_lines = file.read()
|
||||
|
@ -31,12 +45,18 @@ class SystemLogs(Resource):
|
|||
log["message"] = raw_message[3]
|
||||
if raw_message_len > 4 and raw_message[4] != '\n':
|
||||
log['exception'] = raw_message[4].strip('\'').replace(' ', '\u2003\u2003')
|
||||
else:
|
||||
log['exception'] = None
|
||||
logs.append(log)
|
||||
|
||||
logs.reverse()
|
||||
return jsonify(data=logs)
|
||||
return logs
|
||||
|
||||
@authenticate
|
||||
@api_ns_system_logs.doc(parser=None)
|
||||
@api_ns_system_logs.response(204, 'Success')
|
||||
@api_ns_system_logs.response(401, 'Not Authenticated')
|
||||
def delete(self):
|
||||
"""Force log rotation and create a new log file"""
|
||||
empty_log()
|
||||
return '', 204
|
||||
|
|
|
@ -2,16 +2,27 @@
|
|||
|
||||
import apprise
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_system_notifications = Namespace('System Notifications', description='Send test notifications provider message')
|
||||
|
||||
|
||||
@api_ns_system_notifications.hide
|
||||
@api_ns_system_notifications.route('system/notifications')
|
||||
class Notifications(Resource):
|
||||
patch_request_parser = reqparse.RequestParser()
|
||||
patch_request_parser.add_argument('url', type=str, required=True, help='Notifications provider URL')
|
||||
|
||||
@authenticate
|
||||
@api_ns_system_notifications.doc(parser=patch_request_parser)
|
||||
@api_ns_system_notifications.response(204, 'Success')
|
||||
@api_ns_system_notifications.response(401, 'Not Authenticated')
|
||||
def patch(self):
|
||||
url = request.form.get("url")
|
||||
"""Test a notifications provider URL"""
|
||||
args = self.patch_request_parser.parse_args()
|
||||
url = args.get("url")
|
||||
|
||||
asset = apprise.AppriseAsset(async_mode=False)
|
||||
|
||||
|
|
|
@ -5,18 +5,33 @@ import json
|
|||
import os
|
||||
import logging
|
||||
|
||||
from flask import jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, fields
|
||||
|
||||
from app.config import settings
|
||||
from app.get_args import args
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_system_releases = Namespace('System Releases', description='List Bazarr releases from Github')
|
||||
|
||||
|
||||
@api_ns_system_releases.route('system/releases')
|
||||
class SystemReleases(Resource):
|
||||
get_response_model = api_ns_system_releases.model('SystemBackupsGetResponse', {
|
||||
'body': fields.List(fields.String),
|
||||
'name': fields.String(),
|
||||
'date': fields.String(),
|
||||
'prerelease': fields.Boolean(),
|
||||
'current': fields.Boolean(),
|
||||
})
|
||||
|
||||
@authenticate
|
||||
@api_ns_system_releases.marshal_with(get_response_model, envelope='data', code=200)
|
||||
@api_ns_system_releases.doc(parser=None)
|
||||
@api_ns_system_releases.response(200, 'Success')
|
||||
@api_ns_system_releases.response(401, 'Not Authenticated')
|
||||
def get(self):
|
||||
"""Get Bazarr releases"""
|
||||
filtered_releases = []
|
||||
try:
|
||||
with io.open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'r', encoding='UTF-8') as f:
|
||||
|
@ -45,4 +60,4 @@ class SystemReleases(Resource):
|
|||
except Exception:
|
||||
logging.exception(
|
||||
'BAZARR cannot parse releases caching file: ' + os.path.join(args.config_dir, 'config', 'releases.txt'))
|
||||
return jsonify(data=filtered_releases)
|
||||
return filtered_releases
|
||||
|
|
|
@ -1,18 +1,28 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
|
||||
from app.config import settings
|
||||
from app.database import TableShows, TableMovies
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_system_searches = Namespace('System Searches', description='Search for series or movies by name')
|
||||
|
||||
|
||||
@api_ns_system_searches.route('system/searches')
|
||||
class Searches(Resource):
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('query', type=str, required=True, help='Series or movie name to search for')
|
||||
|
||||
@authenticate
|
||||
@api_ns_system_searches.doc(parser=get_request_parser)
|
||||
@api_ns_system_searches.response(200, 'Success')
|
||||
@api_ns_system_searches.response(401, 'Not Authenticated')
|
||||
def get(self):
|
||||
query = request.args.get('query')
|
||||
"""List results from query"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
query = args.get('query')
|
||||
search_list = []
|
||||
|
||||
if query:
|
||||
|
@ -38,4 +48,4 @@ class Searches(Resource):
|
|||
movies = list(movies)
|
||||
search_list += movies
|
||||
|
||||
return jsonify(search_list)
|
||||
return search_list
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import json
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace
|
||||
|
||||
from app.database import TableLanguagesProfiles, TableSettingsLanguages, TableShows, TableMovies, \
|
||||
TableSettingsNotifier
|
||||
|
@ -15,7 +15,11 @@ from subtitles.indexer.movies import list_missing_subtitles_movies
|
|||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_system_settings = Namespace('systemSettings', description='System settings API endpoint')
|
||||
|
||||
|
||||
@api_ns_system_settings.hide
|
||||
@api_ns_system_settings.route('system/settings')
|
||||
class SystemSettings(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
|
|
|
@ -4,8 +4,7 @@ import os
|
|||
import platform
|
||||
import logging
|
||||
|
||||
from flask import jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace
|
||||
from tzlocal import get_localzone_name
|
||||
|
||||
from radarr.info import get_radarr_info
|
||||
|
@ -15,10 +14,14 @@ from init import startTime
|
|||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_system_status = Namespace('System Status', description='List environment information and versions')
|
||||
|
||||
|
||||
@api_ns_system_status.route('system/status')
|
||||
class SystemStatus(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
"""Return environment information and versions"""
|
||||
package_version = ''
|
||||
if 'BAZARR_PACKAGE_VERSION' in os.environ:
|
||||
package_version = os.environ['BAZARR_PACKAGE_VERSION']
|
||||
|
@ -44,4 +47,4 @@ class SystemStatus(Resource):
|
|||
system_status.update({'start_time': startTime})
|
||||
system_status.update({'timezone': timezone})
|
||||
|
||||
return jsonify(data=system_status)
|
||||
return {'data': system_status}
|
||||
|
|
|
@ -1,16 +1,28 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_system = Namespace('System', description='Shutdown or restart Bazarr')
|
||||
|
||||
|
||||
@api_ns_system.hide
|
||||
@api_ns_system.route('system')
|
||||
class System(Resource):
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('action', type=str, required=True,
|
||||
help='Action to perform from ["shutdown", "restart"]')
|
||||
|
||||
@authenticate
|
||||
@api_ns_system.doc(parser=post_request_parser)
|
||||
@api_ns_system.response(204, 'Success')
|
||||
@api_ns_system.response(401, 'Not Authenticated')
|
||||
def post(self):
|
||||
"""Shutdown or restart Bazarr"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
from app.server import webserver
|
||||
action = request.args.get('action')
|
||||
action = args.get('action')
|
||||
if action == "shutdown":
|
||||
webserver.shutdown()
|
||||
elif action == "restart":
|
||||
|
|
|
@ -1,17 +1,37 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse, fields
|
||||
|
||||
from app.scheduler import scheduler
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
api_ns_system_tasks = Namespace('System Tasks', description='List or execute tasks')
|
||||
|
||||
|
||||
@api_ns_system_tasks.route('system/tasks')
|
||||
class SystemTasks(Resource):
|
||||
get_response_model = api_ns_system_tasks.model('SystemBackupsGetResponse', {
|
||||
'interval': fields.String(),
|
||||
'job_id': fields.String(),
|
||||
'job_running': fields.Boolean(),
|
||||
'name': fields.String(),
|
||||
'next_run_in': fields.String(),
|
||||
'next_run_time': fields.String(),
|
||||
})
|
||||
|
||||
get_request_parser = reqparse.RequestParser()
|
||||
get_request_parser.add_argument('taskid', type=str, required=False, help='List tasks or a single task properties')
|
||||
|
||||
@authenticate
|
||||
@api_ns_system_tasks.marshal_with(get_response_model, envelope='data', code=200)
|
||||
@api_ns_system_tasks.doc(parser=None)
|
||||
@api_ns_system_tasks.response(200, 'Success')
|
||||
@api_ns_system_tasks.response(401, 'Not Authenticated')
|
||||
def get(self):
|
||||
taskid = request.args.get('taskid')
|
||||
"""List tasks"""
|
||||
args = self.get_request_parser.parse_args()
|
||||
taskid = args.get('taskid')
|
||||
|
||||
task_list = scheduler.get_task_list()
|
||||
|
||||
|
@ -21,11 +41,19 @@ class SystemTasks(Resource):
|
|||
task_list = [item]
|
||||
continue
|
||||
|
||||
return jsonify(data=task_list)
|
||||
return task_list
|
||||
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('taskid', type=str, required=True, help='Task id of the task to run')
|
||||
|
||||
@authenticate
|
||||
@api_ns_system_tasks.doc(parser=post_request_parser)
|
||||
@api_ns_system_tasks.response(204, 'Success')
|
||||
@api_ns_system_tasks.response(401, 'Not Authenticated')
|
||||
def post(self):
|
||||
taskid = request.form.get('taskid')
|
||||
"""Run task"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
taskid = args.get('taskid')
|
||||
|
||||
scheduler.execute_job_now(taskid)
|
||||
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .plex import WebHooksPlex
|
||||
from .sonarr import WebHooksSonarr
|
||||
from .radarr import WebHooksRadarr
|
||||
from .plex import api_ns_webhooks_plex
|
||||
from .sonarr import api_ns_webhooks_sonarr
|
||||
from .radarr import api_ns_webhooks_radarr
|
||||
|
||||
|
||||
api_bp_webhooks = Blueprint('api_webhooks', __name__)
|
||||
api = Api(api_bp_webhooks)
|
||||
|
||||
api.add_resource(WebHooksPlex, '/webhooks/plex')
|
||||
api.add_resource(WebHooksSonarr, '/webhooks/sonarr')
|
||||
api.add_resource(WebHooksRadarr, '/webhooks/radarr')
|
||||
api_ns_list_webhooks = [
|
||||
api_ns_webhooks_plex,
|
||||
api_ns_webhooks_radarr,
|
||||
api_ns_webhooks_sonarr,
|
||||
]
|
||||
|
|
|
@ -5,8 +5,7 @@ import requests
|
|||
import os
|
||||
import logging
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
from bs4 import BeautifulSoup as bso
|
||||
|
||||
from app.database import TableEpisodes, TableShows, TableMovies
|
||||
|
@ -15,15 +14,31 @@ from subtitles.mass_download import episode_download_subtitles, movies_download_
|
|||
from ..utils import authenticate
|
||||
|
||||
|
||||
api_ns_webhooks_plex = Namespace('Webhooks Plex', description='Webhooks endpoint that can be configured in Plex to '
|
||||
'trigger a subtitles search when playback start.')
|
||||
|
||||
|
||||
@api_ns_webhooks_plex.route('webhooks/plex')
|
||||
class WebHooksPlex(Resource):
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('payload', type=str, required=True, help='Webhook payload')
|
||||
|
||||
@authenticate
|
||||
@api_ns_webhooks_plex.doc(parser=post_request_parser)
|
||||
@api_ns_webhooks_plex.response(200, 'Success')
|
||||
@api_ns_webhooks_plex.response(204, 'Unhandled event')
|
||||
@api_ns_webhooks_plex.response(400, 'No GUID found')
|
||||
@api_ns_webhooks_plex.response(401, 'Not Authenticated')
|
||||
@api_ns_webhooks_plex.response(404, 'IMDB series/movie ID not found')
|
||||
def post(self):
|
||||
json_webhook = request.form.get('payload')
|
||||
"""Trigger subtitles search on play media event in Plex"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
json_webhook = args.get('payload')
|
||||
parsed_json_webhook = json.loads(json_webhook)
|
||||
|
||||
event = parsed_json_webhook['event']
|
||||
if event not in ['media.play']:
|
||||
return '', 204
|
||||
return 'Unhandled event', 204
|
||||
|
||||
media_type = parsed_json_webhook['Metadata']['type']
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
|
||||
from app.database import TableMovies
|
||||
from subtitles.mass_download import movies_download_subtitles
|
||||
|
@ -11,10 +10,23 @@ from utilities.path_mappings import path_mappings
|
|||
from ..utils import authenticate
|
||||
|
||||
|
||||
api_ns_webhooks_radarr = Namespace('Webhooks Radarr', description='Webhooks to trigger subtitles search based on '
|
||||
'Radarr movie file ID')
|
||||
|
||||
|
||||
@api_ns_webhooks_radarr.route('webhooks/radarr')
|
||||
class WebHooksRadarr(Resource):
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('radarr_moviefile_id', type=int, required=True, help='Movie file ID')
|
||||
|
||||
@authenticate
|
||||
@api_ns_webhooks_radarr.doc(parser=post_request_parser)
|
||||
@api_ns_webhooks_radarr.response(200, 'Success')
|
||||
@api_ns_webhooks_radarr.response(401, 'Not Authenticated')
|
||||
def post(self):
|
||||
movie_file_id = request.form.get('radarr_moviefile_id')
|
||||
"""Search for missing subtitles for a specific movie file id"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
movie_file_id = args.get('radarr_moviefile_id')
|
||||
|
||||
radarrMovieId = TableMovies.select(TableMovies.radarrId,
|
||||
TableMovies.path) \
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
|
||||
from app.database import TableEpisodes, TableShows
|
||||
from subtitles.mass_download import episode_download_subtitles
|
||||
|
@ -11,10 +10,23 @@ from utilities.path_mappings import path_mappings
|
|||
from ..utils import authenticate
|
||||
|
||||
|
||||
api_ns_webhooks_sonarr = Namespace('Webhooks Sonarr', description='Webhooks to trigger subtitles search based on '
|
||||
'Sonarr episode file ID')
|
||||
|
||||
|
||||
@api_ns_webhooks_sonarr.route('webhooks/sonarr')
|
||||
class WebHooksSonarr(Resource):
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('sonarr_episodefile_id', type=int, required=True, help='Episode file ID')
|
||||
|
||||
@authenticate
|
||||
@api_ns_webhooks_sonarr.doc(parser=post_request_parser)
|
||||
@api_ns_webhooks_sonarr.response(200, 'Success')
|
||||
@api_ns_webhooks_sonarr.response(401, 'Not Authenticated')
|
||||
def post(self):
|
||||
episode_file_id = request.form.get('sonarr_episodefile_id')
|
||||
"""Search for missing subtitles for a specific episode file id"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
episode_file_id = args.get('sonarr_episodefile_id')
|
||||
|
||||
sonarrEpisodeId = TableEpisodes.select(TableEpisodes.sonarrEpisodeId,
|
||||
TableEpisodes.path) \
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# coding=utf-8
|
||||
|
||||
from flask import Flask, redirect
|
||||
import os
|
||||
|
||||
from flask_cors import CORS
|
||||
from flask_socketio import SocketIO
|
||||
|
@ -21,6 +20,8 @@ def create_app():
|
|||
app.config['JSONIFY_PRETTYPRINT_REGULAR'] = True
|
||||
app.config['JSON_AS_ASCII'] = False
|
||||
|
||||
app.config['RESTX_MASK_SWAGGER'] = False
|
||||
|
||||
if settings.get('cors', 'enabled'):
|
||||
CORS(app)
|
||||
|
||||
|
|
|
@ -215,6 +215,7 @@ class TableShows(BaseModel):
|
|||
audio_language = TextField(null=True)
|
||||
fanart = TextField(null=True)
|
||||
imdbId = TextField(default='""', null=True)
|
||||
monitored = TextField(null=True)
|
||||
overview = TextField(null=True)
|
||||
path = TextField(unique=True)
|
||||
poster = TextField(null=True)
|
||||
|
@ -302,6 +303,7 @@ def migrate_db():
|
|||
migrator.add_column('table_shows', 'seriesType', TextField(default='""', null=True)),
|
||||
migrator.add_column('table_shows', 'imdbId', TextField(default='""', null=True)),
|
||||
migrator.add_column('table_shows', 'profileId', IntegerField(null=True)),
|
||||
migrator.add_column('table_shows', 'monitored', TextField(null=True)),
|
||||
migrator.add_column('table_episodes', 'format', TextField(null=True)),
|
||||
migrator.add_column('table_episodes', 'resolution', TextField(null=True)),
|
||||
migrator.add_column('table_episodes', 'video_codec', TextField(null=True)),
|
||||
|
@ -384,6 +386,7 @@ def get_exclusion_clause(exclusion_type):
|
|||
monitoredOnly = settings.sonarr.getboolean('only_monitored')
|
||||
if monitoredOnly:
|
||||
where_clause.append((TableEpisodes.monitored == 'True'))
|
||||
where_clause.append((TableShows.monitored == 'True'))
|
||||
else:
|
||||
monitoredOnly = settings.radarr.getboolean('only_monitored')
|
||||
if monitoredOnly:
|
||||
|
|
|
@ -4,10 +4,12 @@ import warnings
|
|||
import logging
|
||||
import os
|
||||
import io
|
||||
import errno
|
||||
|
||||
from waitress.server import create_server
|
||||
from time import sleep
|
||||
|
||||
from api import api_bp_list
|
||||
from api import api_bp
|
||||
from .ui import ui_bp
|
||||
from .get_args import args
|
||||
from .config import settings, base_url
|
||||
|
@ -15,10 +17,7 @@ from .database import database
|
|||
from .app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
for item in api_bp_list:
|
||||
ui_bp.register_blueprint(item, url_prefix='/api')
|
||||
|
||||
ui_bp.register_blueprint(api_bp, url_prefix='/api')
|
||||
app.register_blueprint(ui_bp, url_prefix=base_url.rstrip('/'))
|
||||
|
||||
|
||||
|
@ -31,16 +30,39 @@ class Server:
|
|||
# Mute Python3 BrokenPipeError
|
||||
warnings.simplefilter("ignore", BrokenPipeError)
|
||||
|
||||
self.server = create_server(app,
|
||||
host=str(settings.general.ip),
|
||||
port=int(args.port) if args.port else int(settings.general.port),
|
||||
threads=100)
|
||||
self.server = None
|
||||
self.connected = False
|
||||
self.address = str(settings.general.ip)
|
||||
self.port = int(args.port) if args.port else int(settings.general.port)
|
||||
|
||||
while not self.connected:
|
||||
sleep(0.1)
|
||||
self.configure_server()
|
||||
|
||||
def configure_server(self):
|
||||
try:
|
||||
self.server = create_server(app,
|
||||
host=self.address,
|
||||
port=self.port,
|
||||
threads=100)
|
||||
self.connected = True
|
||||
except OSError as error:
|
||||
if error.errno == errno.EADDRNOTAVAIL:
|
||||
logging.exception("BAZARR cannot bind to specified IP, trying with default (0.0.0.0)")
|
||||
self.address = '0.0.0.0'
|
||||
self.connected = False
|
||||
elif error.errno == errno.EADDRINUSE:
|
||||
logging.exception("BAZARR cannot bind to specified TCP port, trying with default (6767)")
|
||||
self.port = '6767'
|
||||
self.connected = False
|
||||
else:
|
||||
logging.exception("BAZARR cannot start because of unhandled exception.")
|
||||
self.shutdown()
|
||||
|
||||
def start(self):
|
||||
try:
|
||||
logging.info(
|
||||
'BAZARR is started and waiting for request on http://' + str(settings.general.ip) + ':' + (str(
|
||||
args.port) if args.port else str(settings.general.port)) + str(base_url))
|
||||
logging.info(f'BAZARR is started and waiting for request on http://{self.server.effective_host}:'
|
||||
f'{self.server.effective_port}')
|
||||
try:
|
||||
self.server.run()
|
||||
except Exception:
|
||||
|
|
|
@ -13,11 +13,13 @@ from collections import deque
|
|||
from time import sleep
|
||||
|
||||
from constants import headers
|
||||
from app.event_handler import event_stream
|
||||
from sonarr.sync.episodes import sync_episodes, sync_one_episode
|
||||
from sonarr.sync.series import update_series, update_one_series
|
||||
from radarr.sync.movies import update_movies, update_one_movie
|
||||
from sonarr.info import get_sonarr_info, url_sonarr
|
||||
from radarr.info import url_radarr
|
||||
from .database import TableShows
|
||||
|
||||
from .config import settings
|
||||
from .scheduler import scheduler
|
||||
|
@ -236,8 +238,17 @@ def dispatcher(data):
|
|||
series_title = data['body']['resource']['title']
|
||||
series_year = data['body']['resource']['year']
|
||||
elif topic == 'episode':
|
||||
series_title = data['body']['resource']['series']['title']
|
||||
series_year = data['body']['resource']['series']['year']
|
||||
if 'series' in data['body']['resource']:
|
||||
series_title = data['body']['resource']['series']['title']
|
||||
series_year = data['body']['resource']['series']['year']
|
||||
else:
|
||||
series_metadata = TableShows.select(TableShows.title, TableShows.year)\
|
||||
.where(TableShows.sonarrSeriesId == data['body']['resource']['seriesId'])\
|
||||
.dicts()\
|
||||
.get_or_none()
|
||||
if series_metadata:
|
||||
series_title = series_metadata['title']
|
||||
series_year = series_metadata['year']
|
||||
episode_title = data['body']['resource']['title']
|
||||
season_number = data['body']['resource']['seasonNumber']
|
||||
episode_number = data['body']['resource']['episodeNumber']
|
||||
|
@ -264,6 +275,7 @@ def dispatcher(data):
|
|||
except Exception as e:
|
||||
logging.debug('BAZARR an exception occurred while parsing SignalR feed: {}'.format(repr(e)))
|
||||
finally:
|
||||
event_stream(type='badges')
|
||||
return
|
||||
|
||||
|
||||
|
|
|
@ -137,6 +137,12 @@ def backup_download(filename):
|
|||
return send_file(os.path.join(settings.backup.folder, filename), cache_timeout=0, as_attachment=True)
|
||||
|
||||
|
||||
@ui_bp.route('/api/swaggerui/static/<path:filename>', methods=['GET'])
|
||||
def swaggerui_static(filename):
|
||||
return send_file(os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'libs', 'flask_restx',
|
||||
'static', filename))
|
||||
|
||||
|
||||
def configured():
|
||||
System.update({System.configured: '1'}).execute()
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
import os
|
||||
|
||||
from threading import Thread
|
||||
|
||||
bazarr_version = 'unknown'
|
||||
|
||||
version_file = os.path.join(os.path.dirname(__file__), '..', 'VERSION')
|
||||
|
@ -14,22 +16,29 @@ os.environ["BAZARR_VERSION"] = bazarr_version.lstrip('v')
|
|||
|
||||
import app.libs # noqa W0611
|
||||
|
||||
from threading import Thread # noqa E402
|
||||
|
||||
from app.get_args import args # noqa E402
|
||||
from app.check_update import apply_update, check_releases, check_if_new_update # noqa E402
|
||||
from app.config import settings, configure_proxy_func, base_url # noqa E402
|
||||
from init import * # noqa E402
|
||||
from app.database import System # noqa E402
|
||||
from app.notifier import update_notifier # noqa E402
|
||||
from languages.get_languages import load_language_in_db # noqa E402
|
||||
from app.signalr_client import sonarr_signalr_client, radarr_signalr_client # noqa E402
|
||||
from app.check_update import apply_update, check_releases # noqa E402
|
||||
from app.server import webserver # noqa E402
|
||||
|
||||
# Install downloaded update
|
||||
if bazarr_version != '':
|
||||
apply_update()
|
||||
check_releases()
|
||||
|
||||
# Check for new update and install latest
|
||||
if args.no_update or not settings.general.getboolean('auto_update'):
|
||||
# user have explicitly requested that we do not update or is using some kind of package/docker that prevent it
|
||||
check_releases()
|
||||
else:
|
||||
# we want to update to the latest version before loading too much stuff. This should prevent deadlock when
|
||||
# there's missing embedded packages after a commit
|
||||
check_if_new_update()
|
||||
|
||||
from app.database import System # noqa E402
|
||||
from app.notifier import update_notifier # noqa E402
|
||||
from languages.get_languages import load_language_in_db # noqa E402
|
||||
from app.signalr_client import sonarr_signalr_client, radarr_signalr_client # noqa E402
|
||||
from app.server import webserver # noqa E402
|
||||
|
||||
configure_proxy_func()
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ def update_movies(send_event=True):
|
|||
|
||||
# Get movies data from radarr
|
||||
movies = get_movies_from_radarr_api(url=url_radarr(), apikey_radarr=apikey_radarr)
|
||||
if not movies:
|
||||
if not isinstance(movies, list):
|
||||
return
|
||||
else:
|
||||
# Get current movies in DB
|
||||
|
@ -219,7 +219,7 @@ def update_one_movie(movie_id, action, defer_search=False):
|
|||
movie = movieParser(movie_data, action='insert', tags_dict=tagsDict,
|
||||
movie_default_profile=movie_default_profile, audio_profiles=audio_profiles)
|
||||
except Exception:
|
||||
logging.debug('BAZARR cannot get movie returned by SignalR feed from Radarr API.')
|
||||
logging.exception('BAZARR cannot get movie returned by SignalR feed from Radarr API.')
|
||||
return
|
||||
|
||||
# Drop useless events
|
||||
|
|
|
@ -198,7 +198,7 @@ def sync_one_episode(episode_id, defer_search=False):
|
|||
episode_file_id=episode_data['episodeFileId'])
|
||||
episode = episodeParser(episode_data)
|
||||
except Exception:
|
||||
logging.debug('BAZARR cannot get episode returned by SignalR feed from Sonarr API.')
|
||||
logging.exception('BAZARR cannot get episode returned by SignalR feed from Sonarr API.')
|
||||
return
|
||||
|
||||
# Drop useless events
|
||||
|
|
|
@ -52,7 +52,8 @@ def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles)
|
|||
'alternateTitles': alternate_titles,
|
||||
'tags': str(tags),
|
||||
'seriesType': show['seriesType'],
|
||||
'imdbId': imdbId}
|
||||
'imdbId': imdbId,
|
||||
'monitored': str(bool(show['monitored']))}
|
||||
else:
|
||||
return {'title': show["title"],
|
||||
'path': show["path"],
|
||||
|
@ -68,7 +69,8 @@ def seriesParser(show, action, tags_dict, serie_default_profile, audio_profiles)
|
|||
'tags': str(tags),
|
||||
'seriesType': show['seriesType'],
|
||||
'imdbId': imdbId,
|
||||
'profileId': serie_default_profile}
|
||||
'profileId': serie_default_profile,
|
||||
'monitored': str(bool(show['monitored']))}
|
||||
|
||||
|
||||
def profile_id_to_language(id_, profiles):
|
||||
|
|
|
@ -37,7 +37,7 @@ def update_series(send_event=True):
|
|||
|
||||
# Get shows data from Sonarr
|
||||
series = get_series_from_sonarr_api(url=url_sonarr(), apikey_sonarr=apikey_sonarr)
|
||||
if not series:
|
||||
if not isinstance(series, list):
|
||||
return
|
||||
else:
|
||||
# Get current shows in DB
|
||||
|
@ -100,7 +100,8 @@ def update_series(send_event=True):
|
|||
TableShows.alternateTitles,
|
||||
TableShows.tags,
|
||||
TableShows.seriesType,
|
||||
TableShows.imdbId).dicts()
|
||||
TableShows.imdbId,
|
||||
TableShows.monitored).dicts()
|
||||
|
||||
for item in series_in_db:
|
||||
series_in_db_list.append(item)
|
||||
|
@ -181,15 +182,15 @@ def update_one_series(series_id, action):
|
|||
return
|
||||
else:
|
||||
if action == 'updated' and existing_series:
|
||||
series = seriesParser(series_data, action='update', tags_dict=tagsDict,
|
||||
series = seriesParser(series_data[0], action='update', tags_dict=tagsDict,
|
||||
serie_default_profile=serie_default_profile,
|
||||
audio_profiles=audio_profiles)
|
||||
elif action == 'updated' and not existing_series:
|
||||
series = seriesParser(series_data, action='insert', tags_dict=tagsDict,
|
||||
series = seriesParser(series_data[0], action='insert', tags_dict=tagsDict,
|
||||
serie_default_profile=serie_default_profile,
|
||||
audio_profiles=audio_profiles)
|
||||
except Exception:
|
||||
logging.debug('BAZARR cannot parse series returned by SignalR feed.')
|
||||
logging.exception('BAZARR cannot get series returned by SignalR feed from Sonarr API.')
|
||||
return
|
||||
|
||||
# Update existing series in DB
|
||||
|
|
|
@ -93,7 +93,7 @@ def get_series_from_sonarr_api(url, apikey_sonarr, sonarr_series_id=None):
|
|||
else:
|
||||
result = r.json()
|
||||
if isinstance(result, dict):
|
||||
return list(result)
|
||||
return [result]
|
||||
else:
|
||||
return r.json()
|
||||
|
||||
|
|
|
@ -18,6 +18,10 @@ from radarr.notify import notify_radarr
|
|||
|
||||
def delete_subtitles(media_type, language, forced, hi, media_path, subtitles_path, sonarr_series_id=None,
|
||||
sonarr_episode_id=None, radarr_id=None):
|
||||
if not subtitles_path:
|
||||
logging.error('No subtitles to delete.')
|
||||
return False
|
||||
|
||||
if not os.path.splitext(subtitles_path)[1] in SUBTITLE_EXTENSIONS:
|
||||
logging.error('BAZARR can only delete subtitles files.')
|
||||
return False
|
||||
|
|
|
@ -55,7 +55,7 @@ class SubSyncer:
|
|||
unparsed_args = [self.reference, '-i', self.srtin, '-o', self.srtout, '--ffmpegpath', self.ffmpeg_path,
|
||||
'--vad', self.vad, '--log-dir-path', self.log_dir_path]
|
||||
if settings.subsync.getboolean('force_audio'):
|
||||
unparsed_args.append('--no-fix-framerate ')
|
||||
unparsed_args.append('--no-fix-framerate')
|
||||
unparsed_args.append('--reference-stream')
|
||||
unparsed_args.append('a:0')
|
||||
if settings.subsync.getboolean('debug'):
|
||||
|
|
|
@ -51,19 +51,6 @@ def manual_upload_subtitle(path, language, forced, hi, title, scene_name, media_
|
|||
if forced:
|
||||
lang_obj = Language.rebuild(lang_obj, forced=True)
|
||||
|
||||
sub = Subtitle(
|
||||
lang_obj,
|
||||
mods=get_array_from(settings.general.subzero_mods)
|
||||
)
|
||||
|
||||
sub.content = subtitle.read()
|
||||
if not sub.is_valid():
|
||||
logging.exception('BAZARR Invalid subtitle file: ' + subtitle.filename)
|
||||
sub.mods = None
|
||||
|
||||
if settings.general.getboolean('utf8_encode'):
|
||||
sub.set_encoding("utf-8")
|
||||
|
||||
if media_type == 'series':
|
||||
episode_metadata = TableEpisodes.select(TableEpisodes.sonarrSeriesId,
|
||||
TableEpisodes.sonarrEpisodeId,
|
||||
|
@ -88,6 +75,20 @@ def manual_upload_subtitle(path, language, forced, hi, title, scene_name, media_
|
|||
else:
|
||||
use_original_format = False
|
||||
|
||||
sub = Subtitle(
|
||||
lang_obj,
|
||||
mods=get_array_from(settings.general.subzero_mods),
|
||||
original_format=use_original_format
|
||||
)
|
||||
|
||||
sub.content = subtitle.read()
|
||||
if not sub.is_valid():
|
||||
logging.exception('BAZARR Invalid subtitle file: ' + subtitle.filename)
|
||||
sub.mods = None
|
||||
|
||||
if settings.general.getboolean('utf8_encode'):
|
||||
sub.set_encoding("utf-8")
|
||||
|
||||
try:
|
||||
sub.format = (get_format_identifier(os.path.splitext(subtitle.filename)[1]),)
|
||||
except Exception:
|
||||
|
|
|
@ -3,31 +3,48 @@
|
|||
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
async function read(path: string, sectionName: string, fieldName: string) {
|
||||
const config = await readFile(path, "utf8");
|
||||
class ConfigReader {
|
||||
config?: string;
|
||||
|
||||
const targetSection = config
|
||||
.split("\n\n")
|
||||
.filter((section) => section.includes(`[${sectionName}]`));
|
||||
|
||||
if (targetSection.length === 0) {
|
||||
throw new Error(`Cannot find [${sectionName}] section in config`);
|
||||
constructor() {
|
||||
this.config = undefined;
|
||||
}
|
||||
|
||||
const section = targetSection[0];
|
||||
|
||||
for (const line of section.split("\n")) {
|
||||
const matched = line.startsWith(fieldName);
|
||||
if (matched) {
|
||||
const results = line.split("=");
|
||||
if (results.length === 2) {
|
||||
const key = results[1].trim();
|
||||
return key;
|
||||
}
|
||||
async open(path: string) {
|
||||
try {
|
||||
this.config = await readFile(path, "utf8");
|
||||
} catch (err) {
|
||||
// We don't want to catch the error here, handle it on getValue method
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Cannot find ${fieldName} in config`);
|
||||
getValue(sectionName: string, fieldName: string) {
|
||||
if (!this.config) {
|
||||
throw new Error("Cannot find config to read");
|
||||
}
|
||||
const targetSection = this.config
|
||||
.split("\n\n")
|
||||
.filter((section) => section.includes(`[${sectionName}]`));
|
||||
|
||||
if (targetSection.length === 0) {
|
||||
throw new Error(`Cannot find [${sectionName}] section in config`);
|
||||
}
|
||||
|
||||
const section = targetSection[0];
|
||||
|
||||
for (const line of section.split("\n")) {
|
||||
const matched = line.startsWith(fieldName);
|
||||
if (matched) {
|
||||
const results = line.split("=");
|
||||
if (results.length === 2) {
|
||||
const key = results[1].trim();
|
||||
return key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Cannot find ${fieldName} in config`);
|
||||
}
|
||||
}
|
||||
|
||||
export default async function overrideEnv(env: Record<string, string>) {
|
||||
|
@ -37,9 +54,12 @@ export default async function overrideEnv(env: Record<string, string>) {
|
|||
return;
|
||||
}
|
||||
|
||||
const reader = new ConfigReader();
|
||||
await reader.open(configPath);
|
||||
|
||||
if (env["VITE_API_KEY"] === undefined) {
|
||||
try {
|
||||
const apiKey = await read(configPath, "auth", "apikey");
|
||||
const apiKey = reader.getValue("auth", "apikey");
|
||||
|
||||
console.log(`Using API key: ${apiKey}`);
|
||||
|
||||
|
@ -54,8 +74,8 @@ export default async function overrideEnv(env: Record<string, string>) {
|
|||
|
||||
if (env["VITE_PROXY_URL"] === undefined) {
|
||||
try {
|
||||
const port = await read(configPath, "general", "port");
|
||||
const baseUrl = await read(configPath, "general", "base_url");
|
||||
const port = reader.getValue("general", "port");
|
||||
const baseUrl = reader.getValue("general", "base_url");
|
||||
|
||||
const url = `http://127.0.0.1:${port}${baseUrl}`;
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<title>Bazarr</title>
|
||||
<base href="{{baseUrl}}" />
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" type="image/x-icon" href="./favicon.ico" />
|
||||
<link rel="icon" type="image/x-icon" href="./images/favicon.ico" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -13,13 +13,17 @@
|
|||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@mantine/core": "^4",
|
||||
"@mantine/hooks": "^4",
|
||||
"@mantine/core": "^5",
|
||||
"@mantine/form": "^5",
|
||||
"@mantine/hooks": "^5",
|
||||
"@mantine/modals": "^5",
|
||||
"@mantine/notifications": "^5",
|
||||
"@mantine/dropzone": "^5",
|
||||
"axios": "^0.27",
|
||||
"react": "^17",
|
||||
"react-dom": "^17",
|
||||
"react-query": "^3",
|
||||
"react-router-dom": "^6",
|
||||
"react-router-dom": "6.3.*",
|
||||
"socket.io-client": "^4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -28,19 +32,17 @@
|
|||
"@fortawesome/free-brands-svg-icons": "^6",
|
||||
"@fortawesome/free-regular-svg-icons": "^6",
|
||||
"@fortawesome/free-solid-svg-icons": "^6",
|
||||
"@fortawesome/react-fontawesome": "^0.1",
|
||||
"@mantine/modals": "^4",
|
||||
"@mantine/notifications": "^4",
|
||||
"@fortawesome/react-fontawesome": "^0.2",
|
||||
"@testing-library/jest-dom": "latest",
|
||||
"@testing-library/react": "12",
|
||||
"@testing-library/react-hooks": "latest",
|
||||
"@testing-library/user-event": "latest",
|
||||
"@types/lodash": "^4",
|
||||
"@types/node": "^17",
|
||||
"@types/node": "^18",
|
||||
"@types/react": "^17",
|
||||
"@types/react-dom": "^17",
|
||||
"@types/react-table": "^7",
|
||||
"@vitejs/plugin-react": "^1.3",
|
||||
"@vitejs/plugin-react": "^2",
|
||||
"clsx": "^1",
|
||||
"eslint": "^8",
|
||||
"eslint-config-react-app": "^7",
|
||||
|
@ -52,7 +54,6 @@
|
|||
"prettier": "^2",
|
||||
"prettier-plugin-organize-imports": "^3",
|
||||
"pretty-quick": "^3",
|
||||
"react-dropzone": "^14",
|
||||
"react-table": "^7",
|
||||
"recharts": "^2",
|
||||
"sass": "^1",
|
||||
|
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -81,8 +81,8 @@ const AppHeader: FunctionComponent = () => {
|
|||
</Group>
|
||||
<Group spacing="xs" position="right" noWrap>
|
||||
<Search></Search>
|
||||
<Menu
|
||||
control={
|
||||
<Menu>
|
||||
<Menu.Target>
|
||||
<Action
|
||||
label="System"
|
||||
tooltip={{ position: "left", openDelay: 2000 }}
|
||||
|
@ -92,24 +92,25 @@ const AppHeader: FunctionComponent = () => {
|
|||
size="lg"
|
||||
variant="light"
|
||||
></Action>
|
||||
}
|
||||
>
|
||||
<Menu.Item
|
||||
icon={<FontAwesomeIcon icon={faArrowRotateLeft} />}
|
||||
onClick={() => restart()}
|
||||
>
|
||||
Restart
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={<FontAwesomeIcon icon={faPowerOff} />}
|
||||
onClick={() => shutdown()}
|
||||
>
|
||||
Shutdown
|
||||
</Menu.Item>
|
||||
<Divider hidden={!hasLogout}></Divider>
|
||||
<Menu.Item hidden={!hasLogout} onClick={() => logout()}>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
icon={<FontAwesomeIcon icon={faArrowRotateLeft} />}
|
||||
onClick={() => restart()}
|
||||
>
|
||||
Restart
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={<FontAwesomeIcon icon={faPowerOff} />}
|
||||
onClick={() => shutdown()}
|
||||
>
|
||||
Shutdown
|
||||
</Menu.Item>
|
||||
<Divider hidden={!hasLogout}></Divider>
|
||||
<Menu.Item hidden={!hasLogout} onClick={() => logout()}>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
</Group>
|
||||
|
|
|
@ -138,7 +138,7 @@ const AppNavbar: FunctionComponent = () => {
|
|||
<Action
|
||||
label="Change Theme"
|
||||
color={dark ? "yellow" : "indigo"}
|
||||
variant="hover"
|
||||
variant="subtle"
|
||||
onClick={() => toggleColorScheme()}
|
||||
icon={dark ? faSun : faMoon}
|
||||
></Action>
|
||||
|
@ -149,7 +149,7 @@ const AppNavbar: FunctionComponent = () => {
|
|||
<Action
|
||||
label="Donate"
|
||||
icon={faHeart}
|
||||
variant="hover"
|
||||
variant="subtle"
|
||||
color="red"
|
||||
></Action>
|
||||
</Anchor>
|
||||
|
@ -254,8 +254,16 @@ const useStyles = createStyles((theme) => {
|
|||
const hoverBackgroundColor =
|
||||
theme.colorScheme === "light" ? theme.colors.gray[0] : theme.colors.dark[7];
|
||||
|
||||
const textColor =
|
||||
theme.colorScheme === "light" ? theme.colors.gray[8] : theme.colors.gray[5];
|
||||
|
||||
return {
|
||||
text: { display: "inline-flex", alignItems: "center", width: "100%" },
|
||||
text: {
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
color: textColor,
|
||||
},
|
||||
anchor: {
|
||||
textDecoration: "none",
|
||||
borderLeft: `2px solid ${borderColor}`,
|
||||
|
@ -273,6 +281,7 @@ const useStyles = createStyles((theme) => {
|
|||
marginLeft: "auto",
|
||||
textDecoration: "none",
|
||||
boxShadow: theme.shadows.xs,
|
||||
color: textColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -323,7 +332,6 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
|
|||
inline
|
||||
p="xs"
|
||||
size="sm"
|
||||
color="gray"
|
||||
weight={primary ? "bold" : "normal"}
|
||||
className={classes.text}
|
||||
>
|
||||
|
@ -336,7 +344,6 @@ const NavbarItem: FunctionComponent<NavbarItemProps> = ({
|
|||
{name}
|
||||
<Badge
|
||||
className={classes.badge}
|
||||
color="gray"
|
||||
radius="xs"
|
||||
hidden={badge === undefined || badge === 0}
|
||||
>
|
||||
|
|
|
@ -5,6 +5,7 @@ import NavbarProvider from "@/contexts/Navbar";
|
|||
import OnlineProvider from "@/contexts/Online";
|
||||
import { notification } from "@/modules/task";
|
||||
import CriticalError from "@/pages/CriticalError";
|
||||
import { RouterNames } from "@/Router/RouterNames";
|
||||
import { Environment } from "@/utilities";
|
||||
import { AppShell } from "@mantine/core";
|
||||
import { useWindowEvent } from "@mantine/hooks";
|
||||
|
@ -26,7 +27,7 @@ const App: FunctionComponent = () => {
|
|||
|
||||
useWindowEvent("app-auth-changed", (ev) => {
|
||||
if (!ev.detail.authenticated) {
|
||||
navigate("/login");
|
||||
navigate(RouterNames.Auth);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
ColorScheme,
|
||||
ColorSchemeProvider,
|
||||
createEmotionCache,
|
||||
MantineProvider,
|
||||
MantineThemeOverride,
|
||||
} from "@mantine/core";
|
||||
|
@ -49,6 +50,8 @@ function useAutoColorScheme() {
|
|||
return { colorScheme, setColorScheme, toggleColorScheme };
|
||||
}
|
||||
|
||||
const emotionCache = createEmotionCache({ key: "bazarr" });
|
||||
|
||||
const ThemeProvider: FunctionComponent = ({ children }) => {
|
||||
const { colorScheme, toggleColorScheme } = useAutoColorScheme();
|
||||
|
||||
|
@ -61,7 +64,7 @@ const ThemeProvider: FunctionComponent = ({ children }) => {
|
|||
withGlobalStyles
|
||||
withNormalizeCSS
|
||||
theme={{ colorScheme, ...theme }}
|
||||
emotionOptions={{ key: "bazarr" }}
|
||||
emotionCache={emotionCache}
|
||||
>
|
||||
{children}
|
||||
</MantineProvider>
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export const RouterNames = {
|
||||
Auth: "/login",
|
||||
NotFound: "/not-found",
|
||||
};
|
|
@ -49,6 +49,7 @@ import {
|
|||
} from "react";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import Redirector from "./Redirector";
|
||||
import { RouterNames } from "./RouterNames";
|
||||
import { CustomRouteObject } from "./type";
|
||||
|
||||
const HistoryStats = lazy(() => import("@/pages/History/Statistics"));
|
||||
|
@ -285,7 +286,7 @@ function useRoutes(): CustomRouteObject[] {
|
|||
],
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
path: RouterNames.Auth,
|
||||
hidden: true,
|
||||
element: <Authentication></Authentication>,
|
||||
},
|
||||
|
|
|
@ -2,7 +2,12 @@ import { useServerSearch } from "@/apis/hooks";
|
|||
import { useDebouncedValue } from "@/utilities";
|
||||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Anchor, Autocomplete, SelectItemProps } from "@mantine/core";
|
||||
import {
|
||||
Anchor,
|
||||
Autocomplete,
|
||||
createStyles,
|
||||
SelectItemProps,
|
||||
} from "@mantine/core";
|
||||
import { forwardRef, FunctionComponent, useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
|
@ -36,12 +41,31 @@ function useSearch(query: string) {
|
|||
);
|
||||
}
|
||||
|
||||
const useStyles = createStyles((theme) => {
|
||||
return {
|
||||
result: {
|
||||
color:
|
||||
theme.colorScheme === "light"
|
||||
? theme.colors.dark[8]
|
||||
: theme.colors.gray[1],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
type ResultCompProps = SelectItemProps & SearchResultItem;
|
||||
|
||||
const ResultComponent = forwardRef<HTMLDivElement, ResultCompProps>(
|
||||
({ link, value }, ref) => {
|
||||
const styles = useStyles();
|
||||
|
||||
return (
|
||||
<Anchor component={Link} to={link} underline={false} color="gray" p="sm">
|
||||
<Anchor
|
||||
component={Link}
|
||||
to={link}
|
||||
underline={false}
|
||||
className={styles.classes.result}
|
||||
p="sm"
|
||||
>
|
||||
{value}
|
||||
</Anchor>
|
||||
);
|
||||
|
|
|
@ -105,7 +105,7 @@ export function useTools() {
|
|||
interface Props {
|
||||
selections: FormType.ModifySubtitle[];
|
||||
children?: ReactElement;
|
||||
menu?: Omit<MenuProps, "control" | "children">;
|
||||
menu?: Omit<MenuProps, "children">;
|
||||
onAction?: (action: "delete" | "search") => void;
|
||||
}
|
||||
|
||||
|
@ -138,70 +138,67 @@ const SubtitleToolsMenu: FunctionComponent<Props> = ({
|
|||
const disabledTools = selections.length === 0;
|
||||
|
||||
return (
|
||||
<Menu
|
||||
control={children}
|
||||
withArrow
|
||||
placement="end"
|
||||
position="left"
|
||||
{...menu}
|
||||
>
|
||||
<Menu.Label>Tools</Menu.Label>
|
||||
{tools.map((tool) => (
|
||||
<Menu withArrow position="left-end" {...menu}>
|
||||
<Menu.Target>{children}</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
<Menu.Label>Tools</Menu.Label>
|
||||
{tools.map((tool) => (
|
||||
<Menu.Item
|
||||
key={tool.key}
|
||||
disabled={disabledTools}
|
||||
icon={<FontAwesomeIcon icon={tool.icon}></FontAwesomeIcon>}
|
||||
onClick={() => {
|
||||
if (tool.modal) {
|
||||
modals.openContextModal(tool.modal, { selections });
|
||||
} else {
|
||||
process(tool.key, tool.name);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{tool.name}
|
||||
</Menu.Item>
|
||||
))}
|
||||
<Divider></Divider>
|
||||
<Menu.Label>Actions</Menu.Label>
|
||||
<Menu.Item
|
||||
key={tool.key}
|
||||
disabled={disabledTools}
|
||||
icon={<FontAwesomeIcon icon={tool.icon}></FontAwesomeIcon>}
|
||||
disabled={selections.length !== 0 || onAction === undefined}
|
||||
icon={<FontAwesomeIcon icon={faSearch}></FontAwesomeIcon>}
|
||||
onClick={() => {
|
||||
if (tool.modal) {
|
||||
modals.openContextModal(tool.modal, { selections });
|
||||
} else {
|
||||
process(tool.key, tool.name);
|
||||
}
|
||||
onAction?.("search");
|
||||
}}
|
||||
>
|
||||
{tool.name}
|
||||
Search
|
||||
</Menu.Item>
|
||||
))}
|
||||
<Divider></Divider>
|
||||
<Menu.Label>Actions</Menu.Label>
|
||||
<Menu.Item
|
||||
disabled={selections.length !== 0 || onAction === undefined}
|
||||
icon={<FontAwesomeIcon icon={faSearch}></FontAwesomeIcon>}
|
||||
onClick={() => {
|
||||
onAction?.("search");
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
disabled={selections.length === 0 || onAction === undefined}
|
||||
color="red"
|
||||
icon={<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>}
|
||||
onClick={() => {
|
||||
modals.openConfirmModal({
|
||||
title: "The following subtitles will be deleted",
|
||||
size: "lg",
|
||||
children: (
|
||||
<ScrollArea style={{ maxHeight: "20rem" }}>
|
||||
<List>
|
||||
{selections.map((s) => (
|
||||
<List.Item my="md" key={s.path}>
|
||||
{s.path}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</ScrollArea>
|
||||
),
|
||||
onConfirm: () => {
|
||||
onAction?.("delete");
|
||||
},
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
confirmProps: { color: "red" },
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete...
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
disabled={selections.length === 0 || onAction === undefined}
|
||||
color="red"
|
||||
icon={<FontAwesomeIcon icon={faTrash}></FontAwesomeIcon>}
|
||||
onClick={() => {
|
||||
modals.openConfirmModal({
|
||||
title: "The following subtitles will be deleted",
|
||||
size: "lg",
|
||||
children: (
|
||||
<ScrollArea style={{ maxHeight: "20rem" }}>
|
||||
<List>
|
||||
{selections.map((s) => (
|
||||
<List.Item my="md" key={s.path}>
|
||||
{s.path}
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</ScrollArea>
|
||||
),
|
||||
onConfirm: () => {
|
||||
onAction?.("delete");
|
||||
},
|
||||
labels: { confirm: "Delete", cancel: "Cancel" },
|
||||
confirmProps: { color: "red" },
|
||||
});
|
||||
}}
|
||||
>
|
||||
Delete...
|
||||
</Menu.Item>
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useCallback, useState } from "react";
|
|||
import { UseMutationResult } from "react-query";
|
||||
|
||||
type MutateButtonProps<DATA, VAR> = Omit<
|
||||
ButtonProps<"button">,
|
||||
ButtonProps,
|
||||
"onClick" | "loading" | "color"
|
||||
> & {
|
||||
mutation: UseMutationResult<DATA, unknown, VAR>;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { FunctionComponent } from "react";
|
|||
|
||||
export type AudioListProps = GroupProps & {
|
||||
audios: Language.Info[];
|
||||
badgeProps?: BadgeProps<"div">;
|
||||
badgeProps?: BadgeProps;
|
||||
};
|
||||
|
||||
const AudioList: FunctionComponent<AudioListProps> = ({
|
||||
|
|
|
@ -2,7 +2,7 @@ import { BuildKey } from "@/utilities";
|
|||
import { Badge, Group, Text, TextProps } from "@mantine/core";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
|
||||
type LanguageTextProps = TextProps<"div"> & {
|
||||
type LanguageTextProps = TextProps & {
|
||||
value: Language.Info;
|
||||
long?: boolean;
|
||||
};
|
||||
|
|
|
@ -2,8 +2,9 @@ import { useSubtitleAction } from "@/apis/hooks";
|
|||
import { Selector, SelectorOption } from "@/components";
|
||||
import { useModals, withModal } from "@/modules/modals";
|
||||
import { task } from "@/modules/task";
|
||||
import FormUtils from "@/utilities/form";
|
||||
import { Button, Divider, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
const TaskName = "Changing Color";
|
||||
|
@ -92,8 +93,11 @@ const ColorToolForm: FunctionComponent<Props> = ({ selections, onSubmit }) => {
|
|||
initialValues: {
|
||||
color: "",
|
||||
},
|
||||
validationRules: {
|
||||
color: (c) => colorOptions.find((op) => op.value === c) !== undefined,
|
||||
validate: {
|
||||
color: FormUtils.validation(
|
||||
(value) => colorOptions.find((op) => op.value === value) !== undefined,
|
||||
"Must select a color"
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { useSubtitleAction } from "@/apis/hooks";
|
||||
import { useModals, withModal } from "@/modules/modals";
|
||||
import { task } from "@/modules/task";
|
||||
import FormUtils from "@/utilities/form";
|
||||
import { Button, Divider, Group, NumberInput, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
const TaskName = "Changing Frame Rate";
|
||||
|
@ -25,9 +26,15 @@ const FrameRateForm: FunctionComponent<Props> = ({ selections, onSubmit }) => {
|
|||
from: 0,
|
||||
to: 0,
|
||||
},
|
||||
validationRules: {
|
||||
from: (v) => v > 0,
|
||||
to: (v) => v > 0,
|
||||
validate: {
|
||||
from: FormUtils.validation(
|
||||
(value) => value > 0,
|
||||
"The From value must be larger than 0"
|
||||
),
|
||||
to: FormUtils.validation(
|
||||
(value) => value > 0,
|
||||
"The To value must be larger than 0"
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { MultiSelector, Selector } from "@/components/inputs";
|
|||
import { useModals, withModal } from "@/modules/modals";
|
||||
import { GetItemId, useSelectorOptions } from "@/utilities";
|
||||
import { Button, Divider, Group, LoadingOverlay, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
import { UseMutationResult } from "react-query";
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useModals, withModal } from "@/modules/modals";
|
|||
import { task, TaskGroup } from "@/modules/task";
|
||||
import { useTableStyles } from "@/styles";
|
||||
import { useArrayAction, useSelectorOptions } from "@/utilities";
|
||||
import FormUtils from "@/utilities/form";
|
||||
import {
|
||||
useLanguageProfileBy,
|
||||
useProfileItemsToLanguages,
|
||||
|
@ -23,7 +24,7 @@ import {
|
|||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { isString } from "lodash";
|
||||
import { FunctionComponent, useEffect, useMemo } from "react";
|
||||
import { Column } from "react-table";
|
||||
|
@ -112,8 +113,8 @@ const MovieUploadForm: FunctionComponent<Props> = ({
|
|||
validateResult: validator(movie, v),
|
||||
})),
|
||||
},
|
||||
validationRules: {
|
||||
files: (values) => {
|
||||
validate: {
|
||||
files: FormUtils.validation((values) => {
|
||||
return (
|
||||
values.find(
|
||||
(v) =>
|
||||
|
@ -122,7 +123,7 @@ const MovieUploadForm: FunctionComponent<Props> = ({
|
|||
v.validateResult.state === "error"
|
||||
) === undefined
|
||||
);
|
||||
},
|
||||
}, "Some files cannot be uploaded, please check"),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@ import { useModals, withModal } from "@/modules/modals";
|
|||
import { useTableStyles } from "@/styles";
|
||||
import { useArrayAction, useSelectorOptions } from "@/utilities";
|
||||
import { LOG } from "@/utilities/console";
|
||||
import FormUtils from "@/utilities/form";
|
||||
import { faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
Accordion,
|
||||
Alert,
|
||||
Button,
|
||||
Checkbox,
|
||||
Stack,
|
||||
|
@ -14,7 +14,7 @@ import {
|
|||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { FunctionComponent, useCallback, useMemo } from "react";
|
||||
import { Column } from "react-table";
|
||||
import ChipInput from "../inputs/ChipInput";
|
||||
|
@ -49,15 +49,14 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
|||
|
||||
const form = useForm({
|
||||
initialValues: profile,
|
||||
validationRules: {
|
||||
name: (value) => value.length > 0,
|
||||
items: (value) => value.length > 0,
|
||||
},
|
||||
errorMessages: {
|
||||
items: (
|
||||
<Alert color="yellow" variant="outline">
|
||||
Must contain at lease 1 language
|
||||
</Alert>
|
||||
validate: {
|
||||
name: FormUtils.validation(
|
||||
(value) => value.length > 0,
|
||||
"Must have a name"
|
||||
),
|
||||
items: FormUtils.validation(
|
||||
(value) => value.length > 0,
|
||||
"Must contain at lease 1 language"
|
||||
),
|
||||
},
|
||||
});
|
||||
|
@ -239,19 +238,18 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
|||
<Stack>
|
||||
<TextInput label="Name" {...form.getInputProps("name")}></TextInput>
|
||||
<Accordion
|
||||
offsetIcon={false}
|
||||
multiple
|
||||
iconPosition="right"
|
||||
initialItem={0}
|
||||
chevronPosition="right"
|
||||
defaultValue={["Languages"]}
|
||||
styles={(theme) => ({
|
||||
contentInner: {
|
||||
content: {
|
||||
[theme.fn.smallerThan("md")]: {
|
||||
padding: 0,
|
||||
},
|
||||
},
|
||||
})}
|
||||
>
|
||||
<Accordion.Item label="Languages">
|
||||
<Accordion.Item value="Languages">
|
||||
<Stack>
|
||||
{form.errors.items}
|
||||
<SimpleTable
|
||||
|
@ -272,7 +270,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
|||
></Selector>
|
||||
</Stack>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item label="Release Info">
|
||||
<Accordion.Item value="Release Info">
|
||||
<Stack>
|
||||
<ChipInput
|
||||
label="Must contain"
|
||||
|
@ -295,7 +293,7 @@ const ProfileEditForm: FunctionComponent<Props> = ({
|
|||
</Text>
|
||||
</Stack>
|
||||
</Accordion.Item>
|
||||
<Accordion.Item label="Subtitles">
|
||||
<Accordion.Item value="Subtitles">
|
||||
<Stack my="xs">
|
||||
<Switch
|
||||
label="Use Original Format"
|
||||
|
|
|
@ -7,6 +7,7 @@ import { useModals, withModal } from "@/modules/modals";
|
|||
import { task, TaskGroup } from "@/modules/task";
|
||||
import { useTableStyles } from "@/styles";
|
||||
import { useArrayAction, useSelectorOptions } from "@/utilities";
|
||||
import FormUtils from "@/utilities/form";
|
||||
import {
|
||||
useLanguageProfileBy,
|
||||
useProfileItemsToLanguages,
|
||||
|
@ -27,7 +28,7 @@ import {
|
|||
Stack,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { isString } from "lodash";
|
||||
import { FunctionComponent, useEffect, useMemo } from "react";
|
||||
import { Column } from "react-table";
|
||||
|
@ -125,15 +126,18 @@ const SeriesUploadForm: FunctionComponent<Props> = ({
|
|||
validateResult: validator(file),
|
||||
})),
|
||||
},
|
||||
validationRules: {
|
||||
files: (values) =>
|
||||
values.find(
|
||||
(v) =>
|
||||
v.language === null ||
|
||||
v.episode === null ||
|
||||
v.validateResult === undefined ||
|
||||
v.validateResult.state === "error"
|
||||
) === undefined,
|
||||
validate: {
|
||||
files: FormUtils.validation(
|
||||
(values) =>
|
||||
values.find(
|
||||
(v) =>
|
||||
v.language === null ||
|
||||
v.episode === null ||
|
||||
v.validateResult === undefined ||
|
||||
v.validateResult.state === "error"
|
||||
) === undefined,
|
||||
"Some files cannot be uploaded, please check"
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import { useSubtitleAction } from "@/apis/hooks";
|
||||
import { useModals, withModal } from "@/modules/modals";
|
||||
import { task } from "@/modules/task";
|
||||
import FormUtils from "@/utilities/form";
|
||||
import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, Divider, Group, NumberInput, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
const TaskName = "Changing Time";
|
||||
|
@ -30,11 +31,14 @@ const TimeOffsetForm: FunctionComponent<Props> = ({ selections, onSubmit }) => {
|
|||
sec: 0,
|
||||
ms: 0,
|
||||
},
|
||||
validationRules: {
|
||||
hour: (v) => v >= 0,
|
||||
min: (v) => v >= 0,
|
||||
sec: (v) => v >= 0,
|
||||
ms: (v) => v >= 0,
|
||||
validate: {
|
||||
hour: FormUtils.validation((v) => v >= 0, "Hour must be larger than 0"),
|
||||
min: FormUtils.validation((v) => v >= 0, "Minute must be larger than 0"),
|
||||
sec: FormUtils.validation((v) => v >= 0, "Second must be larger than 0"),
|
||||
ms: FormUtils.validation(
|
||||
(v) => v >= 0,
|
||||
"Millisecond must be larger than 0"
|
||||
),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -2,9 +2,10 @@ import { useSubtitleAction } from "@/apis/hooks";
|
|||
import { useModals, withModal } from "@/modules/modals";
|
||||
import { task } from "@/modules/task";
|
||||
import { useSelectorOptions } from "@/utilities";
|
||||
import FormUtils from "@/utilities/form";
|
||||
import { useEnabledLanguages } from "@/utilities/languages";
|
||||
import { Alert, Button, Divider, Stack } from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { isObject } from "lodash";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
import { Selector } from "../inputs";
|
||||
|
@ -138,8 +139,8 @@ const TranslationForm: FunctionComponent<Props> = ({
|
|||
initialValues: {
|
||||
language: null as Language.Info | null,
|
||||
},
|
||||
validationRules: {
|
||||
language: isObject,
|
||||
validate: {
|
||||
language: FormUtils.validation(isObject, "Please select a language"),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from "@mantine/core";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
export type ActionProps = ActionIconProps<"button"> & {
|
||||
export type ActionProps = MantineComp<ActionIconProps, "button"> & {
|
||||
icon: IconDefinition;
|
||||
label: string;
|
||||
tooltip?: Omit<TooltipProps, "label" | "children">;
|
||||
|
|
|
@ -26,7 +26,9 @@ const ChipInput: FunctionComponent<ChipInputProps> = ({ ...props }) => {
|
|||
getCreateLabel={(query) => `Add "${query}"`}
|
||||
onCreate={(query) => {
|
||||
onChange?.([...(value ?? []), query]);
|
||||
return query;
|
||||
}}
|
||||
buildOption={(value) => value}
|
||||
></MultiSelector>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import {
|
||||
faArrowUp,
|
||||
faFileCirclePlus,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { createStyles, Group, Stack, Text } from "@mantine/core";
|
||||
import { Dropzone } from "@mantine/dropzone";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
const useStyle = createStyles((theme) => {
|
||||
return {
|
||||
container: {
|
||||
pointerEvents: "none",
|
||||
minHeight: 220,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export const DropContent: FunctionComponent = () => {
|
||||
const { classes } = useStyle();
|
||||
|
||||
return (
|
||||
<Group position="center" spacing="xl" className={classes.container}>
|
||||
<Dropzone.Idle>
|
||||
<FontAwesomeIcon icon={faFileCirclePlus} size="2x" />
|
||||
</Dropzone.Idle>
|
||||
<Dropzone.Accept>
|
||||
<FontAwesomeIcon icon={faArrowUp} size="2x" />
|
||||
</Dropzone.Accept>
|
||||
<Dropzone.Reject>
|
||||
<FontAwesomeIcon icon={faXmark} size="2x" />
|
||||
</Dropzone.Reject>
|
||||
<Stack spacing={0}>
|
||||
<Text size="lg">Upload Subtitles</Text>
|
||||
<Text color="dimmed" size="sm">
|
||||
Attach as many files as you like, you will need to select file
|
||||
metadata before uploading
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
};
|
|
@ -1,129 +0,0 @@
|
|||
import {
|
||||
faArrowUp,
|
||||
faFileCirclePlus,
|
||||
faXmark,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Box, createStyles, Overlay, Stack, Text } from "@mantine/core";
|
||||
import clsx from "clsx";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
import { DropzoneState } from "react-dropzone";
|
||||
|
||||
const useStyle = createStyles((theme) => {
|
||||
return {
|
||||
container: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
inner: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
overflow: "hidden",
|
||||
margin: theme.spacing.md,
|
||||
borderRadius: theme.radius.md,
|
||||
borderWidth: "0.2rem",
|
||||
borderStyle: "dashed",
|
||||
borderColor: theme.colors.gray[7],
|
||||
backgroundColor: theme.fn.rgba(theme.colors.gray[0], 0.4),
|
||||
},
|
||||
accepted: {
|
||||
borderColor: theme.colors.brand[7],
|
||||
backgroundColor: theme.fn.rgba(theme.colors.brand[0], 0.6),
|
||||
},
|
||||
rejected: {
|
||||
borderColor: theme.colors.red[7],
|
||||
backgroundColor: theme.fn.rgba(theme.colors.red[0], 0.9),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
export interface DropOverlayProps {
|
||||
state: DropzoneState;
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
export const DropOverlay: FunctionComponent<DropOverlayProps> = ({
|
||||
state,
|
||||
children,
|
||||
zIndex = 10,
|
||||
}) => {
|
||||
const {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive,
|
||||
isDragAccept: accepted,
|
||||
isDragReject: rejected,
|
||||
} = state;
|
||||
|
||||
const { classes } = useStyle();
|
||||
|
||||
const visible = isDragActive;
|
||||
|
||||
const icon = useMemo(() => {
|
||||
if (accepted) {
|
||||
return faArrowUp;
|
||||
} else if (rejected) {
|
||||
return faXmark;
|
||||
} else {
|
||||
return faFileCirclePlus;
|
||||
}
|
||||
}, [accepted, rejected]);
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (accepted) {
|
||||
return "Release to Upload";
|
||||
} else if (rejected) {
|
||||
return "Cannot Upload Files";
|
||||
} else {
|
||||
return "Upload Subtitles";
|
||||
}
|
||||
}, [accepted, rejected]);
|
||||
|
||||
const subtitle = useMemo(() => {
|
||||
if (accepted) {
|
||||
return "";
|
||||
} else if (rejected) {
|
||||
return "Some files are invalid";
|
||||
} else {
|
||||
return "Drop to upload";
|
||||
}
|
||||
}, [accepted, rejected]);
|
||||
|
||||
return (
|
||||
<Box sx={{ position: "relative" }} {...getRootProps()}>
|
||||
{/* Fix for some browsers. Some browsers need a input element to trigger the file browser panel */}
|
||||
<input {...getInputProps()} hidden />
|
||||
{visible && (
|
||||
<Box className={classes.container} style={{ zIndex }}>
|
||||
<Stack
|
||||
spacing="xs"
|
||||
className={clsx(classes.inner, {
|
||||
[classes.accepted]: accepted,
|
||||
[classes.rejected]: rejected,
|
||||
})}
|
||||
style={{ zIndex: zIndex + 1 }}
|
||||
>
|
||||
<Box>
|
||||
<FontAwesomeIcon icon={icon} size="3x" />
|
||||
</Box>
|
||||
<Text size="xl">{title}</Text>
|
||||
<Text color="gray" size="sm">
|
||||
{subtitle}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Overlay zIndex={zIndex}></Overlay>
|
||||
</Box>
|
||||
)}
|
||||
{children}
|
||||
</Box>
|
||||
);
|
||||
};
|
|
@ -109,6 +109,7 @@ export type MultiSelectorProps<T> = Override<
|
|||
options: readonly SelectorOption<T>[];
|
||||
onChange?: (value: T[]) => void;
|
||||
getkey?: (value: T) => string;
|
||||
buildOption?: (value: string) => T;
|
||||
},
|
||||
Omit<MultiSelectProps, "data">
|
||||
>;
|
||||
|
@ -119,11 +120,15 @@ export function MultiSelector<T>({
|
|||
options,
|
||||
onChange,
|
||||
getkey = DefaultKeyBuilder,
|
||||
buildOption,
|
||||
...select
|
||||
}: MultiSelectorProps<T>) {
|
||||
const labelRef = useRef(getkey);
|
||||
labelRef.current = getkey;
|
||||
|
||||
const buildRef = useRef(buildOption);
|
||||
buildRef.current = buildOption;
|
||||
|
||||
const data = useMemo(
|
||||
() =>
|
||||
options.map<SelectItemWithPayload<T>>(({ value, ...option }) => ({
|
||||
|
@ -150,6 +155,8 @@ export function MultiSelector<T>({
|
|||
const payload = data.find((v) => v.value === value)?.payload;
|
||||
if (payload) {
|
||||
payloads.push(payload);
|
||||
} else if (buildRef.current) {
|
||||
payloads.push(buildRef.current(value));
|
||||
}
|
||||
}
|
||||
onChange?.(payloads);
|
||||
|
@ -159,10 +166,10 @@ export function MultiSelector<T>({
|
|||
|
||||
return (
|
||||
<MultiSelect
|
||||
{...select}
|
||||
value={wrappedValue}
|
||||
defaultValue={wrappedDefaultValue}
|
||||
onChange={wrappedOnChange}
|
||||
{...select}
|
||||
data={data}
|
||||
></MultiSelect>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export { default as Action } from "./Action";
|
||||
export * from "./DropOverlay";
|
||||
export * from "./DropContent";
|
||||
export * from "./FileBrowser";
|
||||
export * from "./Selector";
|
||||
|
|
|
@ -237,40 +237,38 @@ const StateIcon: FunctionComponent<{ matches: string[]; dont: string[] }> = ({
|
|||
const { ref, hovered } = useHover();
|
||||
|
||||
return (
|
||||
<Popover
|
||||
opened={hovered}
|
||||
placement="center"
|
||||
position="top"
|
||||
target={
|
||||
<Popover opened={hovered} position="top" width={360} withArrow>
|
||||
<Popover.Target>
|
||||
<Text color={hasIssues ? "yellow" : "green"} ref={ref}>
|
||||
<FontAwesomeIcon
|
||||
icon={hasIssues ? faExclamationCircle : faCheckCircle}
|
||||
></FontAwesomeIcon>
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
<Group align="flex-start" spacing="xl">
|
||||
<Stack align="flex-start" spacing="xs">
|
||||
<Text color="green">
|
||||
<FontAwesomeIcon icon={faCheck}></FontAwesomeIcon>
|
||||
</Text>
|
||||
<List>
|
||||
{matches.map((v, idx) => (
|
||||
<List.Item key={BuildKey(idx, v, "match")}>{v}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Stack>
|
||||
<Stack align="flex-start" spacing="xs">
|
||||
<Text color="yellow">
|
||||
<FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>
|
||||
</Text>
|
||||
<List>
|
||||
{dont.map((v, idx) => (
|
||||
<List.Item key={BuildKey(idx, v, "miss")}>{v}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<Group position="left" spacing="xl" noWrap grow>
|
||||
<Stack align="flex-start" justify="flex-start" spacing="xs" mb="auto">
|
||||
<Text color="green">
|
||||
<FontAwesomeIcon icon={faCheck}></FontAwesomeIcon>
|
||||
</Text>
|
||||
<List>
|
||||
{matches.map((v, idx) => (
|
||||
<List.Item key={BuildKey(idx, v, "match")}>{v}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Stack>
|
||||
<Stack align="flex-start" justify="flex-start" spacing="xs" mb="auto">
|
||||
<Text color="yellow">
|
||||
<FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>
|
||||
</Text>
|
||||
<List>
|
||||
{dont.map((v, idx) => (
|
||||
<List.Item key={BuildKey(idx, v, "miss")}>{v}</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -2,19 +2,18 @@ import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
|||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { Button, ButtonProps, Text } from "@mantine/core";
|
||||
import {
|
||||
ComponentProps,
|
||||
FunctionComponent,
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
type ToolboxButtonProps = Omit<
|
||||
ButtonProps<"button">,
|
||||
"color" | "variant" | "leftIcon"
|
||||
> & {
|
||||
icon: IconDefinition;
|
||||
children: string;
|
||||
};
|
||||
type ToolboxButtonProps = Omit<ButtonProps, "color" | "variant" | "leftIcon"> &
|
||||
Omit<ComponentProps<"button">, "ref"> & {
|
||||
icon: IconDefinition;
|
||||
children: string;
|
||||
};
|
||||
|
||||
const ToolboxButton: FunctionComponent<ToolboxButtonProps> = ({
|
||||
icon,
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
Stack,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { useForm } from "@mantine/hooks";
|
||||
import { useForm } from "@mantine/form";
|
||||
import { FunctionComponent } from "react";
|
||||
|
||||
const Authentication: FunctionComponent = () => {
|
||||
|
@ -33,7 +33,11 @@ const Authentication: FunctionComponent = () => {
|
|||
src={`${Environment.baseUrl}/images/logo128.png`}
|
||||
></Avatar>
|
||||
<Divider></Divider>
|
||||
<form onSubmit={form.onSubmit(login)}>
|
||||
<form
|
||||
onSubmit={form.onSubmit((values) => {
|
||||
login(values);
|
||||
})}
|
||||
>
|
||||
<Stack>
|
||||
<TextInput
|
||||
placeholder="Username"
|
||||
|
|
|
@ -2,7 +2,7 @@ import { useEpisodeSubtitleModification } from "@/apis/hooks";
|
|||
import Language from "@/components/bazarr/Language";
|
||||
import SubtitleToolsMenu from "@/components/SubtitleToolsMenu";
|
||||
import { task, TaskGroup } from "@/modules/task";
|
||||
import { Badge, FloatingTooltip, MantineColor } from "@mantine/core";
|
||||
import { Badge, MantineColor, Tooltip } from "@mantine/core";
|
||||
import { FunctionComponent, useMemo, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
|
@ -49,11 +49,20 @@ export const Subtitle: FunctionComponent<Props> = ({
|
|||
return list;
|
||||
}, [episodeId, subtitle.code2, subtitle.path]);
|
||||
|
||||
const ctx = (
|
||||
<Badge color={color}>
|
||||
<Language.Text value={subtitle} long={false}></Language.Text>
|
||||
</Badge>
|
||||
);
|
||||
|
||||
if (disabled) {
|
||||
return <Tooltip.Floating label="Embedded Subtitle">{ctx}</Tooltip.Floating>;
|
||||
}
|
||||
|
||||
return (
|
||||
<SubtitleToolsMenu
|
||||
menu={{
|
||||
trigger: "hover",
|
||||
opened: disabled ? false : undefined,
|
||||
onOpen: () => setOpen(true),
|
||||
onClose: () => setOpen(false),
|
||||
}}
|
||||
|
@ -93,11 +102,7 @@ export const Subtitle: FunctionComponent<Props> = ({
|
|||
}
|
||||
}}
|
||||
>
|
||||
<FloatingTooltip label="Embedded Subtitle" disabled={!disabled}>
|
||||
<Badge color={color}>
|
||||
<Language.Text value={subtitle} long={false}></Language.Text>
|
||||
</Badge>
|
||||
</FloatingTooltip>
|
||||
{ctx}
|
||||
</SubtitleToolsMenu>
|
||||
);
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue