# Conflicts: # bazarr/api.py # bazarr/config.py # bazarr/get_movies.py # bazarr/get_providers.py # bazarr/get_subtitle.py # bazarr/init.py # bazarr/list_subtitles.py # bazarr/server.py # bazarr/signalr_client.py # bazarr/utils.py # frontend/src/@types/system.d.ts # libs/version.txt # tests/conftest.py # tests/test_assrt.pyautonomous
commit
dc86c0076d
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,25 @@
|
||||
# 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
|
||||
|
||||
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
|
||||
]
|
@ -0,0 +1,12 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .badges import Badges
|
||||
|
||||
|
||||
api_bp_badges = Blueprint('api_badges', __name__)
|
||||
api = Api(api_bp_badges)
|
||||
|
||||
api.add_resource(Badges, '/badges')
|
@ -0,0 +1,47 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
import operator
|
||||
from functools import reduce
|
||||
|
||||
from database import get_exclusion_clause, TableEpisodes, TableShows, TableMovies
|
||||
from get_providers import get_throttled_providers
|
||||
from utils import get_health_issues
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
class Badges(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
episodes_conditions = [(TableEpisodes.missing_subtitles is not None),
|
||||
(TableEpisodes.missing_subtitles != '[]')]
|
||||
episodes_conditions += get_exclusion_clause('series')
|
||||
missing_episodes = TableEpisodes.select(TableShows.tags,
|
||||
TableShows.seriesType,
|
||||
TableEpisodes.monitored)\
|
||||
.join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\
|
||||
.where(reduce(operator.and_, episodes_conditions))\
|
||||
.count()
|
||||
|
||||
movies_conditions = [(TableMovies.missing_subtitles is not None),
|
||||
(TableMovies.missing_subtitles != '[]')]
|
||||
movies_conditions += get_exclusion_clause('movie')
|
||||
missing_movies = TableMovies.select(TableMovies.tags,
|
||||
TableMovies.monitored)\
|
||||
.where(reduce(operator.and_, movies_conditions))\
|
||||
.count()
|
||||
|
||||
throttled_providers = len(eval(str(get_throttled_providers())))
|
||||
|
||||
health_issues = len(get_health_issues())
|
||||
|
||||
result = {
|
||||
"episodes": missing_episodes,
|
||||
"movies": missing_movies,
|
||||
"providers": throttled_providers,
|
||||
"status": health_issues
|
||||
}
|
||||
return jsonify(result)
|
@ -0,0 +1,20 @@
|
||||
# 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
|
||||
|
||||
|
||||
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')
|
@ -0,0 +1,92 @@
|
||||
# coding=utf-8
|
||||
|
||||
import datetime
|
||||
import pretty
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from database import TableEpisodes, TableShows, TableBlacklist
|
||||
from ..utils import authenticate, postprocessEpisode
|
||||
from utils import blacklist_log, delete_subtitles, blacklist_delete_all, blacklist_delete
|
||||
from helper import path_mappings
|
||||
from get_subtitle import episode_download_subtitles
|
||||
from event_handler import event_stream
|
||||
|
||||
|
||||
# GET: get blacklist
|
||||
# POST: add blacklist
|
||||
# DELETE: remove blacklist
|
||||
class EpisodesBlacklist(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
|
||||
data = TableBlacklist.select(TableShows.title.alias('seriesTitle'),
|
||||
TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'),
|
||||
TableEpisodes.title.alias('episodeTitle'),
|
||||
TableEpisodes.sonarrSeriesId,
|
||||
TableBlacklist.provider,
|
||||
TableBlacklist.subs_id,
|
||||
TableBlacklist.language,
|
||||
TableBlacklist.timestamp)\
|
||||
.join(TableEpisodes, on=(TableBlacklist.sonarr_episode_id == TableEpisodes.sonarrEpisodeId))\
|
||||
.join(TableShows, on=(TableBlacklist.sonarr_series_id == TableShows.sonarrSeriesId))\
|
||||
.order_by(TableBlacklist.timestamp.desc())\
|
||||
.limit(length)\
|
||||
.offset(start)\
|
||||
.dicts()
|
||||
data = list(data)
|
||||
|
||||
for item in data:
|
||||
# Make timestamp pretty
|
||||
item["parsed_timestamp"] = datetime.datetime.fromtimestamp(int(item['timestamp'])).strftime('%x %X')
|
||||
item.update({'timestamp': pretty.date(datetime.datetime.fromtimestamp(item['timestamp']))})
|
||||
|
||||
postprocessEpisode(item)
|
||||
|
||||
return jsonify(data=data)
|
||||
|
||||
@authenticate
|
||||
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')
|
||||
|
||||
episodeInfo = TableEpisodes.select(TableEpisodes.path)\
|
||||
.where(TableEpisodes.sonarrEpisodeId == sonarr_episode_id)\
|
||||
.dicts()\
|
||||
.get()
|
||||
|
||||
media_path = episodeInfo['path']
|
||||
subtitles_path = request.form.get('subtitles_path')
|
||||
|
||||
blacklist_log(sonarr_series_id=sonarr_series_id,
|
||||
sonarr_episode_id=sonarr_episode_id,
|
||||
provider=provider,
|
||||
subs_id=subs_id,
|
||||
language=language)
|
||||
delete_subtitles(media_type='series',
|
||||
language=language,
|
||||
forced=False,
|
||||
hi=False,
|
||||
media_path=path_mappings.path_replace(media_path),
|
||||
subtitles_path=subtitles_path,
|
||||
sonarr_series_id=sonarr_series_id,
|
||||
sonarr_episode_id=sonarr_episode_id)
|
||||
episode_download_subtitles(sonarr_episode_id)
|
||||
event_stream(type='episode-history')
|
||||
return '', 200
|
||||
|
||||
@authenticate
|
||||
def delete(self):
|
||||
if request.args.get("all") == "true":
|
||||
blacklist_delete_all()
|
||||
else:
|
||||
provider = request.form.get('provider')
|
||||
subs_id = request.form.get('subs_id')
|
||||
blacklist_delete(provider=provider, subs_id=subs_id)
|
||||
return '', 204
|
@ -0,0 +1,30 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from database import TableEpisodes
|
||||
from ..utils import authenticate, postprocessEpisode
|
||||
|
||||
|
||||
class Episodes(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
seriesId = request.args.getlist('seriesid[]')
|
||||
episodeId = request.args.getlist('episodeid[]')
|
||||
|
||||
if len(episodeId) > 0:
|
||||
result = TableEpisodes.select().where(TableEpisodes.sonarrEpisodeId.in_(episodeId)).dicts()
|
||||
elif len(seriesId) > 0:
|
||||
result = TableEpisodes.select()\
|
||||
.where(TableEpisodes.sonarrSeriesId.in_(seriesId))\
|
||||
.order_by(TableEpisodes.season.desc(), TableEpisodes.episode.desc())\
|
||||
.dicts()
|
||||
else:
|
||||
return "Series or Episode ID not provided", 400
|
||||
|
||||
result = list(result)
|
||||
for item in result:
|
||||
postprocessEpisode(item)
|
||||
|
||||
return jsonify(data=result)
|
@ -0,0 +1,177 @@
|
||||
# coding=utf-8
|
||||
|
||||
import os
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
from subliminal_patch.core import SUBTITLE_EXTENSIONS
|
||||
|
||||
from database import TableEpisodes, get_audio_profile_languages
|
||||
from ..utils import authenticate
|
||||
from helper import path_mappings
|
||||
from get_providers import get_providers, get_providers_auth
|
||||
from get_subtitle import download_subtitle, manual_upload_subtitle
|
||||
from utils import history_log, delete_subtitles
|
||||
from notifier import send_notifications
|
||||
from list_subtitles import store_subtitles
|
||||
from event_handler import event_stream
|
||||
from config import settings
|
||||
|
||||
|
||||
# PATCH: Download Subtitles
|
||||
# POST: Upload Subtitles
|
||||
# DELETE: Delete Subtitles
|
||||
class EpisodesSubtitles(Resource):
|
||||
@authenticate
|
||||
def patch(self):
|
||||
sonarrSeriesId = request.args.get('seriesid')
|
||||
sonarrEpisodeId = request.args.get('episodeid')
|
||||
episodeInfo = TableEpisodes.select(TableEpisodes.title,
|
||||
TableEpisodes.path,
|
||||
TableEpisodes.scene_name,
|
||||
TableEpisodes.audio_language)\
|
||||
.where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId)\
|
||||
.dicts()\
|
||||
.get()
|
||||
|
||||
title = episodeInfo['title']
|
||||
episodePath = path_mappings.path_replace(episodeInfo['path'])
|
||||
sceneName = episodeInfo['scene_name']
|
||||
audio_language = episodeInfo['audio_language']
|
||||
if sceneName is None: sceneName = "None"
|
||||
|
||||
language = request.form.get('language')
|
||||
hi = request.form.get('hi').capitalize()
|
||||
forced = request.form.get('forced').capitalize()
|
||||
|
||||
providers_list = get_providers()
|
||||
providers_auth = get_providers_auth()
|
||||
|
||||
audio_language_list = get_audio_profile_languages(episode_id=sonarrEpisodeId)
|
||||
if len(audio_language_list) > 0:
|
||||
audio_language = audio_language_list[0]['name']
|
||||
else:
|
||||
audio_language = None
|
||||
|
||||
try:
|
||||
result = download_subtitle(episodePath, language, audio_language, hi, forced, providers_list,
|
||||
providers_auth, sceneName, title, 'series')
|
||||
if result is not None:
|
||||
message = result[0]
|
||||
path = result[1]
|
||||
forced = result[5]
|
||||
if result[8]:
|
||||
language_code = result[2] + ":hi"
|
||||
elif forced:
|
||||
language_code = result[2] + ":forced"
|
||||
else:
|
||||
language_code = result[2]
|
||||
provider = result[3]
|
||||
score = result[4]
|
||||
subs_id = result[6]
|
||||
subs_path = result[7]
|
||||
history_log(1, sonarrSeriesId, sonarrEpisodeId, message, path, language_code, provider, score, subs_id,
|
||||
subs_path)
|
||||
send_notifications(sonarrSeriesId, sonarrEpisodeId, message)
|
||||
store_subtitles(path, episodePath)
|
||||
else:
|
||||
event_stream(type='episode', payload=sonarrEpisodeId)
|
||||
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return '', 204
|
||||
|
||||
@authenticate
|
||||
def post(self):
|
||||
sonarrSeriesId = request.args.get('seriesid')
|
||||
sonarrEpisodeId = request.args.get('episodeid')
|
||||
episodeInfo = TableEpisodes.select(TableEpisodes.title,
|
||||
TableEpisodes.path,
|
||||
TableEpisodes.scene_name,
|
||||
TableEpisodes.audio_language)\
|
||||
.where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId)\
|
||||
.dicts()\
|
||||
.get()
|
||||
|
||||
title = episodeInfo['title']
|
||||
episodePath = path_mappings.path_replace(episodeInfo['path'])
|
||||
sceneName = episodeInfo['scene_name']
|
||||
audio_language = episodeInfo['audio_language']
|
||||
if sceneName is None: sceneName = "None"
|
||||
|
||||
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')
|
||||
|
||||
_, ext = os.path.splitext(subFile.filename)
|
||||
|
||||
if ext not in SUBTITLE_EXTENSIONS:
|
||||
raise ValueError('A subtitle of an invalid format was uploaded.')
|
||||
|
||||
try:
|
||||
result = manual_upload_subtitle(path=episodePath,
|
||||
language=language,
|
||||
forced=forced,
|
||||
hi=hi,
|
||||
title=title,
|
||||
scene_name=sceneName,
|
||||
media_type='series',
|
||||
subtitle=subFile,
|
||||
audio_language=audio_language)
|
||||
|
||||
if result is not None:
|
||||
message = result[0]
|
||||
path = result[1]
|
||||
subs_path = result[2]
|
||||
if hi:
|
||||
language_code = language + ":hi"
|
||||
elif forced:
|
||||
language_code = language + ":forced"
|
||||
else:
|
||||
language_code = language
|
||||
provider = "manual"
|
||||
score = 360
|
||||
history_log(4, sonarrSeriesId, sonarrEpisodeId, message, path, language_code, provider, score,
|
||||
subtitles_path=subs_path)
|
||||
if not settings.general.getboolean('dont_notify_manual_actions'):
|
||||
send_notifications(sonarrSeriesId, sonarrEpisodeId, message)
|
||||
store_subtitles(path, episodePath)
|
||||
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return '', 204
|
||||
|
||||
@authenticate
|
||||
def delete(self):
|
||||
sonarrSeriesId = request.args.get('seriesid')
|
||||
sonarrEpisodeId = request.args.get('episodeid')
|
||||
episodeInfo = TableEpisodes.select(TableEpisodes.title,
|
||||
TableEpisodes.path,
|
||||
TableEpisodes.scene_name,
|
||||
TableEpisodes.audio_language)\
|
||||
.where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId)\
|
||||
.dicts()\
|
||||
.get()
|
||||
|
||||
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')
|
||||
|
||||
subtitlesPath = path_mappings.path_replace_reverse(subtitlesPath)
|
||||
|
||||
delete_subtitles(media_type='series',
|
||||
language=language,
|
||||
forced=forced,
|
||||
hi=hi,
|
||||
media_path=episodePath,
|
||||
subtitles_path=subtitlesPath,
|
||||
sonarr_series_id=sonarrSeriesId,
|
||||
sonarr_episode_id=sonarrEpisodeId)
|
||||
|
||||
return '', 204
|
@ -0,0 +1,133 @@
|
||||
# coding=utf-8
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import operator
|
||||
import pretty
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from functools import reduce
|
||||
from peewee import fn
|
||||
from datetime import timedelta
|
||||
|
||||
from database import get_exclusion_clause, TableEpisodes, TableShows, TableHistory, TableBlacklist
|
||||
from ..utils import authenticate, postprocessEpisode
|
||||
from config import settings
|
||||
from helper import path_mappings
|
||||
|
||||
|
||||
class EpisodesHistory(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
episodeid = request.args.get('episodeid')
|
||||
|
||||
upgradable_episodes_not_perfect = []
|
||||
if settings.general.getboolean('upgrade_subs'):
|
||||
days_to_upgrade_subs = settings.general.days_to_upgrade_subs
|
||||
minimum_timestamp = ((datetime.datetime.now() - timedelta(days=int(days_to_upgrade_subs))) -
|
||||
datetime.datetime(1970, 1, 1)).total_seconds()
|
||||
|
||||
if settings.general.getboolean('upgrade_manual'):
|
||||
query_actions = [1, 2, 3, 6]
|
||||
else:
|
||||
query_actions = [1, 3]
|
||||
|
||||
upgradable_episodes_conditions = [(TableHistory.action.in_(query_actions)),
|
||||
(TableHistory.timestamp > minimum_timestamp),
|
||||
(TableHistory.score is not None)]
|
||||
upgradable_episodes_conditions += get_exclusion_clause('series')
|
||||
upgradable_episodes = TableHistory.select(TableHistory.video_path,
|
||||
fn.MAX(TableHistory.timestamp).alias('timestamp'),
|
||||
TableHistory.score,
|
||||
TableShows.tags,
|
||||
TableEpisodes.monitored,
|
||||
TableShows.seriesType)\
|
||||
.join(TableEpisodes, on=(TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId))\
|
||||
.join(TableShows, on=(TableHistory.sonarrSeriesId == TableShows.sonarrSeriesId))\
|
||||
.where(reduce(operator.and_, upgradable_episodes_conditions))\
|
||||
.group_by(TableHistory.video_path)\
|
||||
.dicts()
|
||||
upgradable_episodes = list(upgradable_episodes)
|
||||
for upgradable_episode in upgradable_episodes:
|
||||
if upgradable_episode['timestamp'] > minimum_timestamp:
|
||||
try:
|
||||
int(upgradable_episode['score'])
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if int(upgradable_episode['score']) < 360:
|
||||
upgradable_episodes_not_perfect.append(upgradable_episode)
|
||||
|
||||
query_conditions = [(TableEpisodes.title is not None)]
|
||||
if episodeid:
|
||||
query_conditions.append((TableEpisodes.sonarrEpisodeId == episodeid))
|
||||
query_condition = reduce(operator.and_, query_conditions)
|
||||
episode_history = TableHistory.select(TableHistory.id,
|
||||
TableShows.title.alias('seriesTitle'),
|
||||
TableEpisodes.monitored,
|
||||
TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'),
|
||||
TableEpisodes.title.alias('episodeTitle'),
|
||||
TableHistory.timestamp,
|
||||
TableHistory.subs_id,
|
||||
TableHistory.description,
|
||||
TableHistory.sonarrSeriesId,
|
||||
TableEpisodes.path,
|
||||
TableHistory.language,
|
||||
TableHistory.score,
|
||||
TableShows.tags,
|
||||
TableHistory.action,
|
||||
TableHistory.subtitles_path,
|
||||
TableHistory.sonarrEpisodeId,
|
||||
TableHistory.provider,
|
||||
TableShows.seriesType)\
|
||||
.join(TableShows, on=(TableHistory.sonarrSeriesId == TableShows.sonarrSeriesId))\
|
||||
.join(TableEpisodes, on=(TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId))\
|
||||
.where(query_condition)\
|
||||
.order_by(TableHistory.timestamp.desc())\
|
||||
.limit(length)\
|
||||
.offset(start)\
|
||||
.dicts()
|
||||
episode_history = list(episode_history)
|
||||
|
||||
blacklist_db = TableBlacklist.select(TableBlacklist.provider, TableBlacklist.subs_id).dicts()
|
||||
blacklist_db = list(blacklist_db)
|
||||
|
||||
for item in episode_history:
|
||||
# Mark episode as upgradable or not
|
||||
item.update({"upgradable": False})
|
||||
if {"video_path": str(item['path']), "timestamp": float(item['timestamp']), "score": str(item['score']),
|
||||
"tags": str(item['tags']), "monitored": str(item['monitored']),
|
||||
"seriesType": str(item['seriesType'])} in upgradable_episodes_not_perfect:
|
||||
if os.path.isfile(path_mappings.path_replace(item['subtitles_path'])):
|
||||
item.update({"upgradable": True})
|
||||
|
||||
del item['path']
|
||||
|
||||
postprocessEpisode(item)
|
||||
|
||||
if item['score']:
|
||||
item['score'] = str(round((int(item['score']) * 100 / 360), 2)) + "%"
|
||||
|
||||
# Make timestamp pretty
|
||||
if item['timestamp']:
|
||||
item["raw_timestamp"] = int(item['timestamp'])
|
||||
item["parsed_timestamp"] = datetime.datetime.fromtimestamp(int(item['timestamp'])).strftime('%x %X')
|
||||
item['timestamp'] = pretty.date(item["raw_timestamp"])
|
||||
|
||||
# Check if subtitles is blacklisted
|
||||
item.update({"blacklisted": False})
|
||||
if item['action'] not in [0, 4, 5]:
|
||||
for blacklisted_item in blacklist_db:
|
||||
if blacklisted_item['provider'] == item['provider'] and \
|
||||
blacklisted_item['subs_id'] == item['subs_id']:
|
||||
item.update({"blacklisted": True})
|
||||
break
|
||||
|
||||
count = TableHistory.select()\
|
||||
.join(TableEpisodes, on=(TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId))\
|
||||
.where(TableEpisodes.title is not None).count()
|
||||
|
||||
return jsonify(data=episode_history, total=count)
|
@ -0,0 +1,74 @@
|
||||
# coding=utf-8
|
||||
|
||||
import operator
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from functools import reduce
|
||||
|
||||
from database import get_exclusion_clause, TableEpisodes, TableShows
|
||||
from ..utils import authenticate, postprocessEpisode
|
||||
|
||||
|
||||
# GET: Get Wanted Episodes
|
||||
class EpisodesWanted(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
episodeid = request.args.getlist('episodeid[]')
|
||||
|
||||
wanted_conditions = [(TableEpisodes.missing_subtitles != '[]')]
|
||||
if len(episodeid) > 0:
|
||||
wanted_conditions.append((TableEpisodes.sonarrEpisodeId in episodeid))
|
||||
wanted_conditions += get_exclusion_clause('series')
|
||||
wanted_condition = reduce(operator.and_, wanted_conditions)
|
||||
|
||||
if len(episodeid) > 0:
|
||||
data = TableEpisodes.select(TableShows.title.alias('seriesTitle'),
|
||||
TableEpisodes.monitored,
|
||||
TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'),
|
||||
TableEpisodes.title.alias('episodeTitle'),
|
||||
TableEpisodes.missing_subtitles,
|
||||
TableEpisodes.sonarrSeriesId,
|
||||
TableEpisodes.sonarrEpisodeId,
|
||||
TableEpisodes.scene_name.alias('sceneName'),
|
||||
TableShows.tags,
|
||||
TableEpisodes.failedAttempts,
|
||||
TableShows.seriesType)\
|
||||
.join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\
|
||||
.where(wanted_condition)\
|
||||
.dicts()
|
||||
else:
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
data = TableEpisodes.select(TableShows.title.alias('seriesTitle'),
|
||||
TableEpisodes.monitored,
|
||||
TableEpisodes.season.concat('x').concat(TableEpisodes.episode).alias('episode_number'),
|
||||
TableEpisodes.title.alias('episodeTitle'),
|
||||
TableEpisodes.missing_subtitles,
|
||||
TableEpisodes.sonarrSeriesId,
|
||||
TableEpisodes.sonarrEpisodeId,
|
||||
TableEpisodes.scene_name.alias('sceneName'),
|
||||
TableShows.tags,
|
||||
TableEpisodes.failedAttempts,
|
||||
TableShows.seriesType)\
|
||||
.join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\
|
||||
.where(wanted_condition)\
|
||||
.order_by(TableEpisodes.rowid.desc())\
|
||||
.limit(length)\
|
||||
.offset(start)\
|
||||
.dicts()
|
||||
data = list(data)
|
||||
|
||||
for item in data:
|
||||
postprocessEpisode(item)
|
||||
|
||||
count_conditions = [(TableEpisodes.missing_subtitles != '[]')]
|
||||
count_conditions += get_exclusion_clause('series')
|
||||
count = TableEpisodes.select(TableShows.tags,
|
||||
TableShows.seriesType,
|
||||
TableEpisodes.monitored)\
|
||||
.join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\
|
||||
.where(reduce(operator.and_, count_conditions))\
|
||||
.count()
|
||||
|
||||
return jsonify(data=data, total=count)
|
@ -0,0 +1,16 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
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')
|
@ -0,0 +1,24 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from filesystem import browse_bazarr_filesystem
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
class BrowseBazarrFS(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
path = request.args.get('path') or ''
|
||||
data = []
|
||||
try:
|
||||
result = browse_bazarr_filesystem(path)
|
||||
if result is None:
|
||||
raise ValueError
|
||||
except Exception:
|
||||
return jsonify([])
|
||||
for item in result['directories']:
|
||||
data.append({'name': item['name'], 'children': True, 'path': item['path']})
|
||||
return jsonify(data)
|
@ -0,0 +1,24 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from filesystem import browse_radarr_filesystem
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
class BrowseRadarrFS(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
path = request.args.get('path') or ''
|
||||
data = []
|
||||
try:
|
||||
result = browse_radarr_filesystem(path)
|
||||
if result is None:
|
||||
raise ValueError
|
||||
except Exception:
|
||||
return jsonify([])
|
||||
for item in result['directories']:
|
||||
data.append({'name': item['name'], 'children': True, 'path': item['path']})
|
||||
return jsonify(data)
|
@ -0,0 +1,24 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from filesystem import browse_sonarr_filesystem
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
class BrowseSonarrFS(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
path = request.args.get('path') or ''
|
||||
data = []
|
||||
try:
|
||||
result = browse_sonarr_filesystem(path)
|
||||
if result is None:
|
||||
raise ValueError
|
||||
except Exception:
|
||||
return jsonify([])
|
||||
for item in result['directories']:
|
||||
data.append({'name': item['name'], 'children': True, 'path': item['path']})
|
||||
return jsonify(data)
|
@ -0,0 +1,12 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .stats import HistoryStats
|
||||
|
||||
|
||||
api_bp_history = Blueprint('api_history', __name__)
|
||||
api = Api(api_bp_history)
|
||||
|
||||
api.add_resource(HistoryStats, '/history/stats')
|
@ -0,0 +1,85 @@
|
||||
# coding=utf-8
|
||||
|
||||
import time
|
||||
import datetime
|
||||
import operator
|
||||
|
||||
from dateutil import rrule
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from functools import reduce
|
||||
from peewee import fn
|
||||
|
||||
from database import TableHistory, TableHistoryMovie
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
class HistoryStats(Resource):
|
||||
@authenticate
|
||||
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'
|
||||
|
||||
# timeframe must be in ['week', 'month', 'trimester', 'year']
|
||||
if timeframe == 'year':
|
||||
delay = 364 * 24 * 60 * 60
|
||||
elif timeframe == 'trimester':
|
||||
delay = 90 * 24 * 60 * 60
|
||||
elif timeframe == 'month':
|
||||
delay = 30 * 24 * 60 * 60
|
||||
elif timeframe == 'week':
|
||||
delay = 6 * 24 * 60 * 60
|
||||
|
||||
now = time.time()
|
||||
past = now - delay
|
||||
|
||||
history_where_clauses = [(TableHistory.timestamp.between(past, now))]
|
||||
history_where_clauses_movie = [(TableHistoryMovie.timestamp.between(past, now))]
|
||||
|
||||
if action != 'All':
|
||||
history_where_clauses.append((TableHistory.action == action))
|
||||
history_where_clauses_movie.append((TableHistoryMovie.action == action))
|
||||
else:
|
||||
history_where_clauses.append((TableHistory.action.in_([1, 2, 3])))
|
||||
history_where_clauses_movie.append((TableHistoryMovie.action.in_([1, 2, 3])))
|
||||
|
||||
if provider != 'All':
|
||||
history_where_clauses.append((TableHistory.provider == provider))
|
||||
history_where_clauses_movie.append((TableHistoryMovie.provider == provider))
|
||||
|
||||
if language != 'All':
|
||||
history_where_clauses.append((TableHistory.language == language))
|
||||
history_where_clauses_movie.append((TableHistoryMovie.language == language))
|
||||
|
||||
history_where_clause = reduce(operator.and_, history_where_clauses)
|
||||
history_where_clause_movie = reduce(operator.and_, history_where_clauses_movie)
|
||||
|
||||
data_series = TableHistory.select(fn.strftime('%Y-%m-%d', TableHistory.timestamp, 'unixepoch').alias('date'),
|
||||
fn.COUNT(TableHistory.id).alias('count'))\
|
||||
.where(history_where_clause) \
|
||||
.group_by(fn.strftime('%Y-%m-%d', TableHistory.timestamp, 'unixepoch'))\
|
||||
.dicts()
|
||||
data_series = list(data_series)
|
||||
|
||||
data_movies = TableHistoryMovie.select(fn.strftime('%Y-%m-%d', TableHistoryMovie.timestamp, 'unixepoch').alias('date'),
|
||||
fn.COUNT(TableHistoryMovie.id).alias('count')) \
|
||||
.where(history_where_clause_movie) \
|
||||
.group_by(fn.strftime('%Y-%m-%d', TableHistoryMovie.timestamp, 'unixepoch')) \
|
||||
.dicts()
|
||||
data_movies = list(data_movies)
|
||||
|
||||
for dt in rrule.rrule(rrule.DAILY,
|
||||
dtstart=datetime.datetime.now() - datetime.timedelta(seconds=delay),
|
||||
until=datetime.datetime.now()):
|
||||
if not any(d['date'] == dt.strftime('%Y-%m-%d') for d in data_series):
|
||||
data_series.append({'date': dt.strftime('%Y-%m-%d'), 'count': 0})
|
||||
if not any(d['date'] == dt.strftime('%Y-%m-%d') for d in data_movies):
|
||||
data_movies.append({'date': dt.strftime('%Y-%m-%d'), 'count': 0})
|
||||
|
||||
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)
|
@ -0,0 +1,20 @@
|
||||
# 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
|
||||
|
||||
|
||||
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')
|
@ -0,0 +1,86 @@
|
||||
# coding=utf-8
|
||||
|
||||
import datetime
|
||||
import pretty
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from database import TableMovies, TableBlacklistMovie
|
||||
from ..utils import authenticate, postprocessMovie
|
||||
from utils import blacklist_log_movie, delete_subtitles, blacklist_delete_all_movie, blacklist_delete_movie
|
||||
from helper import path_mappings
|
||||
from get_subtitle import movies_download_subtitles
|
||||
from event_handler import event_stream
|
||||
|
||||
|
||||
# GET: get blacklist
|
||||
# POST: add blacklist
|
||||
# DELETE: remove blacklist
|
||||
class MoviesBlacklist(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
|
||||
data = TableBlacklistMovie.select(TableMovies.title,
|
||||
TableMovies.radarrId,
|
||||
TableBlacklistMovie.provider,
|
||||
TableBlacklistMovie.subs_id,
|
||||
TableBlacklistMovie.language,
|
||||
TableBlacklistMovie.timestamp)\
|
||||
.join(TableMovies, on=(TableBlacklistMovie.radarr_id == TableMovies.radarrId))\
|
||||
.order_by(TableBlacklistMovie.timestamp.desc())\
|
||||
.limit(length)\
|
||||
.offset(start)\
|
||||
.dicts()
|
||||
data = list(data)
|
||||
|
||||
for item in data:
|
||||
postprocessMovie(item)
|
||||
|
||||
# Make timestamp pretty
|
||||
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)
|
||||
|
||||
@authenticate
|
||||
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')
|
||||
# TODO
|
||||
forced = False
|
||||
hi = False
|
||||
|
||||
data = TableMovies.select(TableMovies.path).where(TableMovies.radarrId == radarr_id).dicts().get()
|
||||
|
||||
media_path = data['path']
|
||||
subtitles_path = request.form.get('subtitles_path')
|
||||
|
||||
blacklist_log_movie(radarr_id=radarr_id,
|
||||
provider=provider,
|
||||
subs_id=subs_id,
|
||||
language=language)
|
||||
delete_subtitles(media_type='movie',
|
||||
language=language,
|
||||
forced=forced,
|
||||
hi=hi,
|
||||
media_path=path_mappings.path_replace_movie(media_path),
|
||||
subtitles_path=subtitles_path,
|
||||
radarr_id=radarr_id)
|
||||
movies_download_subtitles(radarr_id)
|
||||
event_stream(type='movie-history')
|
||||
return '', 200
|
||||
|
||||
@authenticate
|
||||
def delete(self):
|
||||
if request.args.get("all") == "true":
|
||||
blacklist_delete_all_movie()
|
||||
else:
|
||||
provider = request.form.get('provider')
|
||||
subs_id = request.form.get('subs_id')
|
||||
blacklist_delete_movie(provider=provider, subs_id=subs_id)
|
||||
return '', 200
|
@ -0,0 +1,129 @@
|
||||
# coding=utf-8
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import operator
|
||||
import pretty
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from functools import reduce
|
||||
from peewee import fn
|
||||
from datetime import timedelta
|
||||
|
||||
from database import get_exclusion_clause, TableMovies, TableHistoryMovie, TableBlacklistMovie
|
||||
from ..utils import authenticate, postprocessMovie
|
||||
from config import settings
|
||||
from helper import path_mappings
|
||||
|
||||
|
||||
class MoviesHistory(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
radarrid = request.args.get('radarrid')
|
||||
|
||||
upgradable_movies = []
|
||||
upgradable_movies_not_perfect = []
|
||||
if settings.general.getboolean('upgrade_subs'):
|
||||
days_to_upgrade_subs = settings.general.days_to_upgrade_subs
|
||||
minimum_timestamp = ((datetime.datetime.now() - timedelta(days=int(days_to_upgrade_subs))) -
|
||||
datetime.datetime(1970, 1, 1)).total_seconds()
|
||||
|
||||
if settings.general.getboolean('upgrade_manual'):
|
||||
query_actions = [1, 2, 3, 6]
|
||||
else:
|
||||
query_actions = [1, 3]
|
||||
|
||||
upgradable_movies_conditions = [(TableHistoryMovie.action.in_(query_actions)),
|
||||
(TableHistoryMovie.timestamp > minimum_timestamp),
|
||||
(TableHistoryMovie.score is not None)]
|
||||
upgradable_movies_conditions += get_exclusion_clause('movie')
|
||||
upgradable_movies = TableHistoryMovie.select(TableHistoryMovie.video_path,
|
||||
fn.MAX(TableHistoryMovie.timestamp).alias('timestamp'),
|
||||
TableHistoryMovie.score,
|
||||
TableMovies.tags,
|
||||
TableMovies.monitored)\
|
||||
.join(TableMovies, on=(TableHistoryMovie.radarrId == TableMovies.radarrId))\
|
||||
.where(reduce(operator.and_, upgradable_movies_conditions))\
|
||||
.group_by(TableHistoryMovie.video_path)\
|
||||
.dicts()
|
||||
upgradable_movies = list(upgradable_movies)
|
||||
|
||||
for upgradable_movie in upgradable_movies:
|
||||
if upgradable_movie['timestamp'] > minimum_timestamp:
|
||||
try:
|
||||
int(upgradable_movie['score'])
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if int(upgradable_movie['score']) < 120:
|
||||
upgradable_movies_not_perfect.append(upgradable_movie)
|
||||
|
||||
query_conditions = [(TableMovies.title is not None)]
|
||||
if radarrid:
|
||||
query_conditions.append((TableMovies.radarrId == radarrid))
|
||||
query_condition = reduce(operator.and_, query_conditions)
|
||||
|
||||
movie_history = TableHistoryMovie.select(TableHistoryMovie.id,
|
||||
TableHistoryMovie.action,
|
||||
TableMovies.title,
|
||||
TableHistoryMovie.timestamp,
|
||||
TableHistoryMovie.description,
|
||||
TableHistoryMovie.radarrId,
|
||||
TableMovies.monitored,
|
||||
TableHistoryMovie.video_path.alias('path'),
|
||||
TableHistoryMovie.language,
|
||||
TableMovies.tags,
|
||||
TableHistoryMovie.score,
|
||||
TableHistoryMovie.subs_id,
|
||||
TableHistoryMovie.provider,
|
||||
TableHistoryMovie.subtitles_path)\
|
||||
.join(TableMovies, on=(TableHistoryMovie.radarrId == TableMovies.radarrId))\
|
||||
.where(query_condition)\
|
||||
.order_by(TableHistoryMovie.timestamp.desc())\
|
||||
.limit(length)\
|
||||
.offset(start)\
|
||||
.dicts()
|
||||
movie_history = list(movie_history)
|
||||
|
||||
blacklist_db = TableBlacklistMovie.select(TableBlacklistMovie.provider, TableBlacklistMovie.subs_id).dicts()
|
||||
blacklist_db = list(blacklist_db)
|
||||
|
||||
for item in movie_history:
|
||||
# Mark movies as upgradable or not
|
||||
item.update({"upgradable": False})
|
||||
if {"video_path": str(item['path']), "timestamp": float(item['timestamp']), "score": str(item['score']),
|
||||
"tags": str(item['tags']), "monitored": str(item['monitored'])} in upgradable_movies_not_perfect:
|
||||
if os.path.isfile(path_mappings.path_replace_movie(item['subtitles_path'])):
|
||||
item.update({"upgradable": True})
|
||||
|
||||
del item['path']
|
||||
|
||||
postprocessMovie(item)
|
||||
|
||||
if item['score']:
|
||||
item['score'] = str(round((int(item['score']) * 100 / 120), 2)) + "%"
|
||||
|
||||
# Make timestamp pretty
|
||||
if item['timestamp']:
|
||||
item["raw_timestamp"] = int(item['timestamp'])
|
||||
item["parsed_timestamp"] = datetime.datetime.fromtimestamp(int(item['timestamp'])).strftime('%x %X')
|
||||
item['timestamp'] = pretty.date(item["raw_timestamp"])
|
||||
|
||||
# Check if subtitles is blacklisted
|
||||
item.update({"blacklisted": False})
|
||||
if item['action'] not in [0, 4, 5]:
|
||||
for blacklisted_item in blacklist_db:
|
||||
if blacklisted_item['provider'] == item['provider'] and blacklisted_item['subs_id'] == item[
|
||||
'subs_id']:
|
||||
item.update({"blacklisted": True})
|
||||
break
|
||||
|
||||
count = TableHistoryMovie.select()\
|
||||
.join(TableMovies, on=(TableHistoryMovie.radarrId == TableMovies.radarrId))\
|
||||
.where(TableMovies.title is not None)\
|
||||
.count()
|
||||
|
||||
return jsonify(data=movie_history, total=count)
|
@ -0,0 +1,80 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from database import TableMovies
|
||||
from ..utils import authenticate, postprocessMovie, None_Keys
|
||||
from list_subtitles import list_missing_subtitles_movies, movies_scan_subtitles
|
||||
from event_handler import event_stream
|
||||
from get_subtitle import movies_download_subtitles, wanted_search_missing_subtitles_movies
|
||||
|
||||
|
||||
class Movies(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
radarrId = request.args.getlist('radarrid[]')
|
||||
|
||||
count = TableMovies.select().count()
|
||||
|
||||
if len(radarrId) != 0:
|
||||
result = TableMovies.select()\
|
||||
.where(TableMovies.radarrId.in_(radarrId))\
|
||||
.order_by(TableMovies.sortTitle)\
|
||||
.dicts()
|
||||
else:
|
||||
result = TableMovies.select().order_by(TableMovies.sortTitle).limit(length).offset(start).dicts()
|
||||
result = list(result)
|
||||
for item in result:
|
||||
postprocessMovie(item)
|
||||
|
||||
return jsonify(data=result, total=count)
|
||||
|
||||
@authenticate
|
||||
def post(self):
|
||||
radarrIdList = request.form.getlist('radarrid')
|
||||
profileIdList = request.form.getlist('profileid')
|
||||
|
||||
for idx in range(len(radarrIdList)):
|
||||
radarrId = radarrIdList[idx]
|
||||
profileId = profileIdList[idx]
|
||||
|
||||
if profileId in None_Keys:
|
||||
profileId = None
|
||||
else:
|
||||
try:
|
||||
profileId = int(profileId)
|
||||
except Exception:
|
||||
return '', 400
|
||||
|
||||
TableMovies.update({
|
||||
TableMovies.profileId: profileId
|
||||
})\
|
||||
.where(TableMovies.radarrId == radarrId)\
|
||||
.execute()
|
||||
|
||||
list_missing_subtitles_movies(no=radarrId, send_event=False)
|
||||
|
||||
event_stream(type='movie', payload=radarrId)
|
||||
event_stream(type='movie-wanted', payload=radarrId)
|
||||
event_stream(type='badges')
|
||||
|
||||
return '', 204
|
||||
|
||||
@authenticate
|
||||
def patch(self):
|
||||
radarrid = request.form.get('radarrid')
|
||||
action = request.form.get('action')
|
||||
if action == "scan-disk":
|
||||
movies_scan_subtitles(radarrid)
|
||||
return '', 204
|
||||
elif action == "search-missing":
|
||||
movies_download_subtitles(radarrid)
|
||||
return '', 204
|
||||
elif action == "search-wanted":
|
||||
wanted_search_missing_subtitles_movies()
|
||||
return '', 204
|
||||
|
||||
return '', 400
|
@ -0,0 +1,175 @@
|
||||
# coding=utf-8
|
||||
|
||||
import os
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
from subliminal_patch.core import SUBTITLE_EXTENSIONS
|
||||
|
||||
from database import TableMovies, get_audio_profile_languages
|
||||
from ..utils import authenticate
|
||||
from helper import path_mappings
|
||||
from get_providers import get_providers, get_providers_auth
|
||||
from get_subtitle import download_subtitle, manual_upload_subtitle
|
||||
from utils import history_log_movie, delete_subtitles
|
||||
from notifier import send_notifications_movie
|
||||
from list_subtitles import store_subtitles_movie
|
||||
from event_handler import event_stream
|
||||
from config import settings
|
||||
|
||||
|
||||
# PATCH: Download Subtitles
|
||||
# POST: Upload Subtitles
|
||||
# DELETE: Delete Subtitles
|
||||
class MoviesSubtitles(Resource):
|
||||
@authenticate
|
||||
def patch(self):
|
||||
# Download
|
||||
radarrId = request.args.get('radarrid')
|
||||
|
||||
movieInfo = TableMovies.select(TableMovies.title,
|
||||
TableMovies.path,
|
||||
TableMovies.sceneName,
|
||||
TableMovies.audio_language)\
|
||||
.where(TableMovies.radarrId == radarrId)\
|
||||
.dicts()\
|
||||
.get()
|
||||
|
||||
moviePath = path_mappings.path_replace_movie(movieInfo['path'])
|
||||
sceneName = movieInfo['sceneName']
|
||||
if sceneName is None: sceneName = 'None'
|
||||
|
||||
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()
|
||||
|
||||
providers_list = get_providers()
|
||||
providers_auth = get_providers_auth()
|
||||
|
||||
audio_language_list = get_audio_profile_languages(movie_id=radarrId)
|
||||
if len(audio_language_list) > 0:
|
||||
audio_language = audio_language_list[0]['name']
|
||||
else:
|
||||
audio_language = None
|
||||
|
||||
try:
|
||||
result = download_subtitle(moviePath, language, audio_language, hi, forced, providers_list,
|
||||
providers_auth, sceneName, title, 'movie')
|
||||
if result is not None:
|
||||
message = result[0]
|
||||
path = result[1]
|
||||
forced = result[5]
|
||||
if result[8]:
|
||||
language_code = result[2] + ":hi"
|
||||
elif forced:
|
||||
language_code = result[2] + ":forced"
|
||||
else:
|
||||
language_code = result[2]
|
||||
provider = result[3]
|
||||
score = result[4]
|
||||
subs_id = result[6]
|
||||
subs_path = result[7]
|
||||
history_log_movie(1, radarrId, message, path, language_code, provider, score, subs_id, subs_path)
|
||||
send_notifications_movie(radarrId, message)
|
||||
store_subtitles_movie(path, moviePath)
|
||||
else:
|
||||
event_stream(type='movie', payload=radarrId)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return '', 204
|
||||
|
||||
@authenticate
|
||||
def post(self):
|
||||
# Upload
|
||||
# TODO: Support Multiply Upload
|
||||
radarrId = request.args.get('radarrid')
|
||||
movieInfo = TableMovies.select(TableMovies.title,
|
||||
TableMovies.path,
|
||||
TableMovies.sceneName,
|
||||
TableMovies.audio_language) \
|
||||
.where(TableMovies.radarrId == radarrId) \
|
||||
.dicts() \
|
||||
.get()
|
||||
|
||||
moviePath = path_mappings.path_replace_movie(movieInfo['path'])
|
||||
sceneName = movieInfo['sceneName']
|
||||
if sceneName is None: sceneName = 'None'
|
||||
|
||||
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')
|
||||
|
||||
_, ext = os.path.splitext(subFile.filename)
|
||||
|
||||
if ext not in SUBTITLE_EXTENSIONS:
|
||||
raise ValueError('A subtitle of an invalid format was uploaded.')
|
||||
|
||||
try:
|
||||
result = manual_upload_subtitle(path=moviePath,
|
||||
language=language,
|
||||
forced=forced,
|
||||
hi=hi,
|
||||
title=title,
|
||||
scene_name=sceneName,
|
||||
media_type='movie',
|
||||
subtitle=subFile,
|
||||
audio_language=audioLanguage)
|
||||
|
||||
if result is not None:
|
||||
message = result[0]
|
||||
path = result[1]
|
||||
subs_path = result[2]
|
||||
if hi:
|
||||
language_code = language + ":hi"
|
||||
elif forced:
|
||||
language_code = language + ":forced"
|
||||
else:
|
||||
language_code = language
|
||||
provider = "manual"
|
||||
score = 120
|
||||
history_log_movie(4, radarrId, message, path, language_code, provider, score, subtitles_path=subs_path)
|
||||
if not settings.general.getboolean('dont_notify_manual_actions'):
|
||||
send_notifications_movie(radarrId, message)
|
||||
store_subtitles_movie(path, moviePath)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return '', 204
|
||||
|
||||
@authenticate
|
||||
def delete(self):
|
||||
# Delete
|
||||
radarrId = request.args.get('radarrid')
|
||||
movieInfo = TableMovies.select(TableMovies.path) \
|
||||
.where(TableMovies.radarrId == radarrId) \
|
||||
.dicts() \
|
||||
.get()
|
||||
|
||||
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')
|
||||
|
||||
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
|
@ -0,0 +1,62 @@
|
||||
# coding=utf-8
|
||||
|
||||
import operator
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from functools import reduce
|
||||
|
||||
from database import get_exclusion_clause, TableMovies
|
||||
from ..utils import authenticate, postprocessMovie
|
||||
|
||||
|
||||
# GET: Get Wanted Movies
|
||||
class MoviesWanted(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
radarrid = request.args.getlist("radarrid[]")
|
||||
|
||||
wanted_conditions = [(TableMovies.missing_subtitles != '[]')]
|
||||
if len(radarrid) > 0:
|
||||
wanted_conditions.append((TableMovies.radarrId.in_(radarrid)))
|
||||
wanted_conditions += get_exclusion_clause('movie')
|
||||
wanted_condition = reduce(operator.and_, wanted_conditions)
|
||||
|
||||
if len(radarrid) > 0:
|
||||
result = TableMovies.select(TableMovies.title,
|
||||
TableMovies.missing_subtitles,
|
||||
TableMovies.radarrId,
|
||||
TableMovies.sceneName,
|
||||
TableMovies.failedAttempts,
|
||||
TableMovies.tags,
|
||||
TableMovies.monitored)\
|
||||
.where(wanted_condition)\
|
||||
.dicts()
|
||||
else:
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
result = TableMovies.select(TableMovies.title,
|
||||
TableMovies.missing_subtitles,
|
||||
TableMovies.radarrId,
|
||||
TableMovies.sceneName,
|
||||
TableMovies.failedAttempts,
|
||||
TableMovies.tags,
|
||||
TableMovies.monitored)\
|
||||
.where(wanted_condition)\
|
||||
.order_by(TableMovies.rowid.desc())\
|
||||
.limit(length)\
|
||||
.offset(start)\
|
||||
.dicts()
|
||||
result = list(result)
|
||||
|
||||
for item in result:
|
||||
postprocessMovie(item)
|
||||
|
||||
count_conditions = [(TableMovies.missing_subtitles != '[]')]
|
||||
count_conditions += get_exclusion_clause('movie')
|
||||
count = TableMovies.select(TableMovies.monitored,
|
||||
TableMovies.tags)\
|
||||
.where(reduce(operator.and_, count_conditions))\
|
||||
.count()
|
||||
|
||||
return jsonify(data=result, total=count)
|
@ -0,0 +1,16 @@
|
||||
# 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
|
||||
|
||||
|
||||
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')
|
@ -0,0 +1,52 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from operator import itemgetter
|
||||
|
||||
from database import TableHistory, TableHistoryMovie
|
||||
from get_providers import list_throttled_providers, reset_throttled_providers
|
||||
from ..utils import authenticate, False_Keys
|
||||
|
||||
|
||||
class Providers(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
history = request.args.get('history')
|
||||
if history and history not in False_Keys:
|
||||
providers = list(TableHistory.select(TableHistory.provider)
|
||||
.where(TableHistory.provider != None and TableHistory.provider != "manual")
|
||||
.dicts())
|
||||
providers += list(TableHistoryMovie.select(TableHistoryMovie.provider)
|
||||
.where(TableHistoryMovie.provider != None and TableHistoryMovie.provider != "manual")
|
||||
.dicts())
|
||||
providers_list = list(set([x['provider'] for x in providers]))
|
||||
providers_dicts = []
|
||||
for provider in providers_list:
|
||||
providers_dicts.append({
|
||||
'name': provider,
|
||||
'status': 'History',
|
||||
'retry': '-'
|
||||
})
|
||||
return jsonify(data=sorted(providers_dicts, key=itemgetter('name')))
|
||||
|
||||
throttled_providers = list_throttled_providers()
|
||||
|
||||
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)
|
||||
|
||||
@authenticate
|
||||
def post(self):
|
||||
action = request.form.get('action')
|
||||
|
||||
if action == 'reset':
|
||||
reset_throttled_providers()
|
||||
return '', 204
|
||||
|
||||
return '', 400
|
@ -0,0 +1,103 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from database import TableEpisodes, TableShows, get_audio_profile_languages
|
||||
from helper import path_mappings
|
||||
from get_providers import get_providers, get_providers_auth
|
||||
from get_subtitle import manual_search, manual_download_subtitle
|
||||
from utils import history_log
|
||||
from config import settings
|
||||
from notifier import send_notifications
|
||||
from list_subtitles import store_subtitles
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
class ProviderEpisodes(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
# Manual Search
|
||||
sonarrEpisodeId = request.args.get('episodeid')
|
||||
episodeInfo = TableEpisodes.select(TableEpisodes.title,
|
||||
TableEpisodes.path,
|
||||
TableEpisodes.scene_name,
|
||||
TableShows.profileId) \
|
||||
.join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId))\
|
||||
.where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId) \
|
||||
.dicts() \
|
||||
.get()
|
||||
|
||||
title = episodeInfo['title']
|
||||
episodePath = path_mappings.path_replace(episodeInfo['path'])
|
||||
sceneName = episodeInfo['scene_name']
|
||||
profileId = episodeInfo['profileId']
|
||||
if sceneName is None: sceneName = "None"
|
||||
|
||||
providers_list = get_providers()
|
||||
providers_auth = get_providers_auth()
|
||||
|
||||
data = manual_search(episodePath, profileId, providers_list, providers_auth, sceneName, title,
|
||||
'series')
|
||||
if not data:
|
||||
data = []
|
||||
return jsonify(data=data)
|
||||
|
||||
@authenticate
|
||||
def post(self):
|
||||
# Manual Download
|
||||
sonarrSeriesId = request.args.get('seriesid')
|
||||
sonarrEpisodeId = request.args.get('episodeid')
|
||||
episodeInfo = TableEpisodes.select(TableEpisodes.title,
|
||||
TableEpisodes.path,
|
||||
TableEpisodes.scene_name) \
|
||||
.where(TableEpisodes.sonarrEpisodeId == sonarrEpisodeId) \
|
||||
.dicts() \
|
||||
.get()
|
||||
|
||||
title = episodeInfo['title']
|
||||
episodePath = path_mappings.path_replace(episodeInfo['path'])
|
||||
sceneName = episodeInfo['scene_name']
|
||||
if sceneName is None: sceneName = "None"
|
||||
|
||||
language = request.form.get('language')
|
||||
hi = request.form.get('hi').capitalize()
|
||||
forced = request.form.get('forced').capitalize()
|
||||
selected_provider = request.form.get('provider')
|
||||
subtitle = request.form.get('subtitle')
|
||||
providers_auth = get_providers_auth()
|
||||
|
||||
audio_language_list = get_audio_profile_languages(episode_id=sonarrEpisodeId)
|
||||
if len(audio_language_list) > 0:
|
||||
audio_language = audio_language_list[0]['name']
|
||||
else:
|
||||
audio_language = 'None'
|
||||
|
||||
try:
|
||||
result = manual_download_subtitle(episodePath, language, audio_language, hi, forced, subtitle,
|
||||
selected_provider, providers_auth, sceneName, title, 'series')
|
||||
if result is not None:
|
||||
message = result[0]
|
||||
path = result[1]
|
||||
forced = result[5]
|
||||
if result[8]:
|
||||
language_code = result[2] + ":hi"
|
||||
elif forced:
|
||||
language_code = result[2] + ":forced"
|
||||
else:
|
||||
language_code = result[2]
|
||||
provider = result[3]
|
||||
score = result[4]
|
||||
subs_id = result[6]
|
||||
subs_path = result[7]
|
||||
history_log(2, sonarrSeriesId, sonarrEpisodeId, message, path, language_code, provider, score, subs_id,
|
||||
subs_path)
|
||||
if not settings.general.getboolean('dont_notify_manual_actions'):
|
||||
send_notifications(sonarrSeriesId, sonarrEpisodeId, message)
|
||||
store_subtitles(path, episodePath)
|
||||
return result, 201
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return '', 204
|
@ -0,0 +1,102 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from database import TableMovies, get_audio_profile_languages
|
||||
from helper import path_mappings
|
||||
from get_providers import get_providers, get_providers_auth
|
||||
from get_subtitle import manual_search, manual_download_subtitle
|
||||
from utils import history_log_movie
|
||||
from config import settings
|
||||
from notifier import send_notifications_movie
|
||||
from list_subtitles import store_subtitles_movie
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
class ProviderMovies(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
# Manual Search
|
||||
radarrId = request.args.get('radarrid')
|
||||
movieInfo = TableMovies.select(TableMovies.title,
|
||||
TableMovies.path,
|
||||
TableMovies.sceneName,
|
||||
TableMovies.profileId) \
|
||||
.where(TableMovies.radarrId == radarrId) \
|
||||
.dicts() \
|
||||
.get()
|
||||
|
||||
title = movieInfo['title']
|
||||
moviePath = path_mappings.path_replace_movie(movieInfo['path'])
|
||||
sceneName = movieInfo['sceneName']
|
||||
profileId = movieInfo['profileId']
|
||||
if sceneName is None: sceneName = "None"
|
||||
|
||||
providers_list = get_providers()
|
||||
providers_auth = get_providers_auth()
|
||||
|
||||
data = manual_search(moviePath, profileId, providers_list, providers_auth, sceneName, title,
|
||||
'movie')
|
||||
if not data:
|
||||
data = []
|
||||
return jsonify(data=data)
|
||||
|
||||
@authenticate
|
||||
def post(self):
|
||||
# Manual Download
|
||||
radarrId = request.args.get('radarrid')
|
||||
movieInfo = TableMovies.select(TableMovies.title,
|
||||
TableMovies.path,
|
||||
TableMovies.sceneName,
|
||||
TableMovies.audio_language) \
|
||||
.where(TableMovies.radarrId == radarrId) \
|
||||
.dicts() \
|
||||
.get()
|
||||
|
||||
title = movieInfo['title']
|
||||
moviePath = path_mappings.path_replace_movie(movieInfo['path'])
|
||||
sceneName = movieInfo['sceneName']
|
||||
if sceneName is None: sceneName = "None"
|
||||
audio_language = movieInfo['audio_language']
|
||||
|
||||
language = request.form.get('language')
|
||||
hi = request.form.get('hi').capitalize()
|
||||
forced = request.form.get('forced').capitalize()
|
||||
selected_provider = request.form.get('provider')
|
||||
subtitle = request.form.get('subtitle')
|
||||
|
||||
providers_auth = get_providers_auth()
|
||||
|
||||
audio_language_list = get_audio_profile_languages(movie_id=radarrId)
|
||||
if len(audio_language_list) > 0:
|
||||
audio_language = audio_language_list[0]['name']
|
||||
else:
|
||||
audio_language = 'None'
|
||||
|
||||
try:
|
||||
result = manual_download_subtitle(moviePath, language, audio_language, hi, forced, subtitle,
|
||||
selected_provider, providers_auth, sceneName, title, 'movie')
|
||||
if result is not None:
|
||||
message = result[0]
|
||||
path = result[1]
|
||||
forced = result[5]
|
||||
if result[8]:
|
||||
language_code = result[2] + ":hi"
|
||||
elif forced:
|
||||
language_code = result[2] + ":forced"
|
||||
else:
|
||||
language_code = result[2]
|
||||
provider = result[3]
|
||||
score = result[4]
|
||||
subs_id = result[6]
|
||||
subs_path = result[7]
|
||||
history_log_movie(2, radarrId, message, path, language_code, provider, score, subs_id, subs_path)
|
||||
if not settings.general.getboolean('dont_notify_manual_actions'):
|
||||
send_notifications_movie(radarrId, message)
|
||||
store_subtitles_movie(path, moviePath)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return '', 204
|
@ -0,0 +1,12 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .series import Series
|
||||
|
||||
|
||||
api_bp_series = Blueprint('api_series', __name__)
|
||||
api = Api(api_bp_series)
|
||||
|
||||
api.add_resource(Series, '/series')
|
@ -0,0 +1,114 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
import operator
|
||||
from functools import reduce
|
||||
|
||||
from database import get_exclusion_clause, TableEpisodes, TableShows
|
||||
from list_subtitles import list_missing_subtitles, series_scan_subtitles
|
||||
from get_subtitle import series_download_subtitles, wanted_search_missing_subtitles_series
|
||||
from ..utils import authenticate, postprocessSeries, None_Keys
|
||||
from event_handler import event_stream
|
||||
|
||||
|
||||
class Series(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
start = request.args.get('start') or 0
|
||||
length = request.args.get('length') or -1
|
||||
seriesId = request.args.getlist('seriesid[]')
|
||||
|
||||
count = TableShows.select().count()
|
||||
|
||||
if len(seriesId) != 0:
|
||||
result = TableShows.select() \
|
||||
.where(TableShows.sonarrSeriesId.in_(seriesId)) \
|
||||
.order_by(TableShows.sortTitle).dicts()
|
||||
else:
|
||||
result = TableShows.select().order_by(TableShows.sortTitle).limit(length).offset(start).dicts()
|
||||
|
||||
result = list(result)
|
||||
|
||||
for item in result:
|
||||
postprocessSeries(item)
|
||||
|
||||
# Add missing subtitles episode count
|
||||
episodes_missing_conditions = [(TableEpisodes.sonarrSeriesId == item['sonarrSeriesId']),
|
||||
(TableEpisodes.missing_subtitles != '[]')]
|
||||
episodes_missing_conditions += get_exclusion_clause('series')
|
||||
|
||||
episodeMissingCount = TableEpisodes.select(TableShows.tags,
|
||||
TableEpisodes.monitored,
|
||||
TableShows.seriesType) \
|
||||
.join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId)) \
|
||||
.where(reduce(operator.and_, episodes_missing_conditions)) \
|
||||
.count()
|
||||
item.update({"episodeMissingCount": episodeMissingCount})
|
||||
|
||||
# Add episode count
|
||||
episodeFileCount = TableEpisodes.select(TableShows.tags,
|
||||
TableEpisodes.monitored,
|
||||
TableShows.seriesType) \
|
||||
.join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId)) \
|
||||
.where(TableEpisodes.sonarrSeriesId == item['sonarrSeriesId']) \
|
||||
.count()
|
||||
item.update({"episodeFileCount": episodeFileCount})
|
||||
|
||||
return jsonify(data=result, total=count)
|
||||
|
||||
@authenticate
|
||||
def post(self):
|
||||
seriesIdList = request.form.getlist('seriesid')
|
||||
profileIdList = request.form.getlist('profileid')
|
||||
|
||||
for idx in range(len(seriesIdList)):
|
||||
seriesId = seriesIdList[idx]
|
||||
profileId = profileIdList[idx]
|
||||
|
||||
if profileId in None_Keys:
|
||||
profileId = None
|
||||
else:
|
||||
try:
|
||||
profileId = int(profileId)
|
||||
except Exception:
|
||||
return '', 400
|
||||
|
||||
TableShows.update({
|
||||
TableShows.profileId: profileId
|
||||
}) \
|
||||
.where(TableShows.sonarrSeriesId == seriesId) \
|
||||
.execute()
|
||||
|
||||
list_missing_subtitles(no=seriesId, send_event=False)
|
||||
|
||||
event_stream(type='series', payload=seriesId)
|
||||
|
||||
episode_id_list = TableEpisodes \
|
||||
.select(TableEpisodes.sonarrEpisodeId) \
|
||||
.where(TableEpisodes.sonarrSeriesId == seriesId) \
|
||||
.dicts()
|
||||
|
||||
for item in episode_id_list:
|
||||
event_stream(type='episode-wanted', payload=item['sonarrEpisodeId'])
|
||||
|
||||
event_stream(type='badges')
|
||||
|
||||
return '', 204
|
||||
|
||||
@authenticate
|
||||
def patch(self):
|
||||
seriesid = request.form.get('seriesid')
|
||||
action = request.form.get('action')
|
||||
if action == "scan-disk":
|
||||
series_scan_subtitles(seriesid)
|
||||
return '', 204
|
||||
elif action == "search-missing":
|
||||
series_download_subtitles(seriesid)
|
||||
return '', 204
|
||||
elif action == "search-wanted":
|
||||
wanted_search_missing_subtitles_series()
|
||||
return '', 204
|
||||
|
||||
return '', 400
|
@ -0,0 +1,14 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .subtitles import Subtitles
|
||||
from .subtitles_info import SubtitleNameInfo
|
||||
|
||||
|
||||
api_bp_subtitles = Blueprint('api_subtitles', __name__)
|
||||
api = Api(api_bp_subtitles)
|
||||
|
||||
api.add_resource(Subtitles, '/subtitles')
|
||||
api.add_resource(SubtitleNameInfo, '/subtitles/info')
|
@ -0,0 +1,72 @@
|
||||
# coding=utf-8
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
|
||||
from database import TableEpisodes, TableMovies
|
||||
from helper import path_mappings
|
||||
from ..utils import authenticate
|
||||
from subsyncer import subsync
|
||||
from utils import translate_subtitles_file, subtitles_apply_mods
|
||||
from get_subtitle import store_subtitles, store_subtitles_movie
|
||||
from config import settings
|
||||
|
||||
|
||||
class Subtitles(Resource):
|
||||
@authenticate
|
||||
def patch(self):
|
||||
action = request.args.get('action')
|
||||
|
||||
language = request.form.get('language')
|
||||
subtitles_path = request.form.get('path')
|
||||
media_type = request.form.get('type')
|
||||
id = request.form.get('id')
|
||||
|
||||
if media_type == 'episode':
|
||||
subtitles_path = path_mappings.path_replace(subtitles_path)
|
||||
metadata = TableEpisodes.select(TableEpisodes.path, TableEpisodes.sonarrSeriesId)\
|
||||
.where(TableEpisodes.sonarrEpisodeId == id)\
|
||||
.dicts()\
|
||||
.get()
|
||||
video_path = path_mappings.path_replace(metadata['path'])
|
||||
else:
|
||||
subtitles_path = path_mappings.path_replace_movie(subtitles_path)
|
||||
metadata = TableMovies.select(TableMovies.path).where(TableMovies.radarrId == id).dicts().get()
|
||||
video_path = path_mappings.path_replace_movie(metadata['path'])
|
||||
|
||||
if action == 'sync':
|
||||
if media_type == 'episode':
|
||||
subsync.sync(video_path=video_path, srt_path=subtitles_path,
|
||||
srt_lang=language, media_type='series', sonarr_series_id=metadata['sonarrSeriesId'],
|
||||
sonarr_episode_id=int(id))
|
||||
else:
|
||||
subsync.sync(video_path=video_path, srt_path=subtitles_path,
|
||||
srt_lang=language, media_type='movies', radarr_id=id)
|
||||
elif action == 'translate':
|
||||
dest_language = language
|
||||
forced = True if request.form.get('forced') == 'true' else False
|
||||
hi = True if request.form.get('hi') == 'true' else False
|
||||
result = translate_subtitles_file(video_path=video_path, source_srt_file=subtitles_path,
|
||||
to_lang=dest_language,
|
||||
forced=forced, hi=hi)
|
||||
if result:
|
||||
if media_type == 'episode':
|
||||
store_subtitles(path_mappings.path_replace_reverse(video_path), video_path)
|
||||
else:
|
||||
store_subtitles_movie(path_mappings.path_replace_reverse_movie(video_path), video_path)
|
||||
return '', 200
|
||||
else:
|
||||
return '', 404
|
||||
else:
|
||||
subtitles_apply_mods(language, subtitles_path, [action])
|
||||
|
||||
# apply chmod if required
|
||||
chmod = int(settings.general.chmod, 8) if not sys.platform.startswith(
|
||||
'win') and settings.general.getboolean('chmod_enabled') else None
|
||||
if chmod:
|
||||
os.chmod(subtitles_path, chmod)
|
||||
|
||||
return '', 204
|
@ -0,0 +1,41 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
from subliminal_patch.core import guessit
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
class SubtitleNameInfo(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
names = request.args.getlist('filenames[]')
|
||||
results = []
|
||||
for name in names:
|
||||
opts = dict()
|
||||
opts['type'] = 'episode'
|
||||
guessit_result = guessit(name, options=opts)
|
||||
result = {}
|
||||
result['filename'] = name
|
||||
if 'subtitle_language' in guessit_result:
|
||||
result['subtitle_language'] = str(guessit_result['subtitle_language'])
|
||||
|
||||
result['episode'] = 0
|
||||
if 'episode' in guessit_result:
|
||||
if isinstance(guessit_result['episode'], list):
|
||||
# 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'])
|
||||
|
||||
if 'season' in guessit_result:
|
||||
result['season'] = int(guessit_result['season'])
|
||||
else:
|
||||
result['season'] = 0
|
||||
|
||||
results.append(result)
|
||||
|
||||
return jsonify(data=results)
|
@ -0,0 +1,33 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .system import System
|
||||
from .searches import Searches
|
||||
from .account import SystemAccount
|
||||
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(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')
|
@ -0,0 +1,29 @@
|
||||
# coding=utf-8
|
||||
|
||||
import gc
|
||||
|
||||
from flask import request, session
|
||||
from flask_restful import Resource
|
||||
|
||||
from config import settings
|
||||
from utils import check_credentials
|
||||
|
||||
|
||||
class SystemAccount(Resource):
|
||||
def post(self):
|
||||
if settings.auth.type != 'form':
|
||||
return '', 405
|
||||
|
||||
action = request.args.get('action')
|
||||
if action == 'login':
|
||||
username = request.form.get('username')
|
||||
password = request.form.get('password')
|
||||
if check_credentials(username, password):
|
||||
session['logged_in'] = True
|
||||
return '', 204
|
||||
elif action == 'logout':
|
||||
session.clear()
|
||||
gc.collect()
|
||||
return '', 204
|
||||
|
||||
return '', 401
|
@ -0,0 +1,13 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from ..utils import authenticate
|
||||
from utils import get_health_issues
|
||||
|
||||
|
||||
class SystemHealth(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
return jsonify(data=get_health_issues())
|
@ -0,0 +1,54 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
from ..utils import authenticate, False_Keys
|
||||
from database import TableHistory, TableHistoryMovie, TableSettingsLanguages
|
||||
from get_languages import alpha2_from_alpha3, language_from_alpha2
|
||||
|
||||
|
||||
class Languages(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
history = request.args.get('history')
|
||||
if history and history not in False_Keys:
|
||||
languages = list(TableHistory.select(TableHistory.language)
|
||||
.where(TableHistory.language != None)
|
||||
.dicts())
|
||||
languages += list(TableHistoryMovie.select(TableHistoryMovie.language)
|
||||
.where(TableHistoryMovie.language != None)
|
||||
.dicts())
|
||||
languages_list = list(set([l['language'].split(':')[0] for l in languages]))
|
||||
languages_dicts = []
|
||||
for language in languages_list:
|
||||
code2 = None
|
||||
if len(language) == 2:
|
||||
code2 = language
|
||||
elif len(language) == 3:
|
||||
code2 = alpha2_from_alpha3(language)
|
||||
else:
|
||||
continue
|
||||
|
||||
if not any(x['code2'] == code2 for x in languages_dicts):
|
||||
try:
|
||||
languages_dicts.append({
|
||||
'code2': code2,
|
||||
'name': language_from_alpha2(code2),
|
||||
# Compatibility: Use false temporarily
|
||||
'enabled': False
|
||||
})
|
||||
except:
|
||||
continue
|
||||
return jsonify(sorted(languages_dicts, key=itemgetter('name')))
|
||||
|
||||
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)
|
@ -0,0 +1,13 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from ..utils import authenticate
|
||||
from database import get_profiles_list
|
||||
|
||||
|
||||
class LanguagesProfiles(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
return jsonify(get_profiles_list())
|
@ -0,0 +1,41 @@
|
||||
# coding=utf-8
|
||||
|
||||
import io
|
||||
import os
|
||||
|
||||
from flask import jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from ..utils import authenticate
|
||||
from logger import empty_log
|
||||
from get_args import args
|
||||
|
||||
|
||||
class SystemLogs(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
logs = []
|
||||
with io.open(os.path.join(args.config_dir, 'log', 'bazarr.log'), encoding='UTF-8') as file:
|
||||
raw_lines = file.read()
|
||||
lines = raw_lines.split('|\n')
|
||||
for line in lines:
|
||||
if line == '':
|
||||
continue
|
||||
raw_message = line.split('|')
|
||||
raw_message_len = len(raw_message)
|
||||
if raw_message_len > 3:
|
||||
log = dict()
|
||||
log["timestamp"] = raw_message[0]
|
||||
log["type"] = raw_message[1].rstrip()
|
||||
log["message"] = raw_message[3]
|
||||
if raw_message_len > 4 and raw_message[4] != '\n':
|
||||
log['exception'] = raw_message[4].strip('\'').replace(' ', '\u2003\u2003')
|
||||
logs.append(log)
|
||||
|
||||
logs.reverse()
|
||||
return jsonify(data=logs)
|
||||
|
||||
@authenticate
|
||||
def delete(self):
|
||||
empty_log()
|
||||
return '', 204
|
@ -0,0 +1,27 @@
|
||||
# coding=utf-8
|
||||
|
||||
import apprise
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
class Notifications(Resource):
|
||||
@authenticate
|
||||
def patch(self):
|
||||
url = request.form.get("url")
|
||||
|
||||
asset = apprise.AppriseAsset(async_mode=False)
|
||||
|
||||
apobj = apprise.Apprise(asset=asset)
|
||||
|
||||
apobj.add(url)
|
||||
|
||||
apobj.notify(
|
||||
title='Bazarr test notification',
|
||||
body='Test notification'
|
||||
)
|
||||
|
||||
return '', 204
|
@ -0,0 +1,47 @@
|
||||
# coding=utf-8
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import logging
|
||||
|
||||
from flask import jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from ..utils import authenticate
|
||||
from config import settings
|
||||
from get_args import args
|
||||
|
||||
|
||||
class SystemReleases(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
filtered_releases = []
|
||||
try:
|
||||
with io.open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'r', encoding='UTF-8') as f:
|
||||
releases = json.loads(f.read())
|
||||
|
||||
for release in releases:
|
||||
if settings.general.branch == 'master' and not release['prerelease']:
|
||||
filtered_releases.append(release)
|
||||
elif settings.general.branch != 'master' and any(not x['prerelease'] for x in filtered_releases):
|
||||
continue
|
||||
elif settings.general.branch != 'master':
|
||||
filtered_releases.append(release)
|
||||
if settings.general.branch == 'master':
|
||||
filtered_releases = filtered_releases[:5]
|
||||
|
||||
current_version = os.environ["BAZARR_VERSION"]
|
||||
|
||||
for i, release in enumerate(filtered_releases):
|
||||
body = release['body'].replace('- ', '').split('\n')[1:]
|
||||
filtered_releases[i] = {"body": body,
|
||||
"name": release['name'],
|
||||
"date": release['date'][:10],
|
||||
"prerelease": release['prerelease'],
|
||||
"current": release['name'].lstrip('v') == current_version}
|
||||
|
||||
except Exception:
|
||||
logging.exception(
|
||||
'BAZARR cannot parse releases caching file: ' + os.path.join(args.config_dir, 'config', 'releases.txt'))
|
||||
return jsonify(data=filtered_releases)
|
@ -0,0 +1,40 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from ..utils import authenticate
|
||||
from config import settings
|
||||
from database import TableShows, TableMovies
|
||||
|
||||
|
||||
class Searches(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
query = request.args.get('query')
|
||||
search_list = []
|
||||
|
||||
if query:
|
||||
if settings.general.getboolean('use_sonarr'):
|
||||
# Get matching series
|
||||
series = TableShows.select(TableShows.title,
|
||||
TableShows.sonarrSeriesId,
|
||||
TableShows.year)\
|
||||
.where(TableShows.title.contains(query))\
|
||||
.order_by(TableShows.title)\
|
||||
.dicts()
|
||||
series = list(series)
|
||||
search_list += series
|
||||
|
||||
if settings.general.getboolean('use_radarr'):
|
||||
# Get matching movies
|
||||
movies = TableMovies.select(TableMovies.title,
|
||||
TableMovies.radarrId,
|
||||
TableMovies.year) \
|
||||
.where(TableMovies.title.contains(query)) \
|
||||
.order_by(TableMovies.title) \
|
||||
.dicts()
|
||||
movies = list(movies)
|
||||
search_list += movies
|
||||
|
||||
return jsonify(search_list)
|
@ -0,0 +1,102 @@
|
||||
# coding=utf-8
|
||||
|
||||
import json
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from ..utils import authenticate
|
||||
from database import TableLanguagesProfiles, TableSettingsLanguages, TableShows, TableMovies, TableSettingsNotifier, \
|
||||
update_profile_id_list
|
||||
from event_handler import event_stream
|
||||
from config import settings, save_settings, get_settings
|
||||
from scheduler import scheduler
|
||||
from list_subtitles import list_missing_subtitles, list_missing_subtitles_movies
|
||||
|
||||
|
||||
class SystemSettings(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
data = get_settings()
|
||||
|
||||
notifications = TableSettingsNotifier.select().order_by(TableSettingsNotifier.name).dicts()
|
||||
notifications = list(notifications)
|
||||
for i, item in enumerate(notifications):
|
||||
item["enabled"] = item["enabled"] == 1
|
||||
notifications[i] = item
|
||||
|
||||
data['notifications'] = dict()
|
||||
data['notifications']['providers'] = notifications
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
@authenticate
|
||||
def post(self):
|
||||
enabled_languages = request.form.getlist('languages-enabled')
|
||||
if len(enabled_languages) != 0:
|
||||
TableSettingsLanguages.update({
|
||||
TableSettingsLanguages.enabled: 0
|
||||
}).execute()
|
||||
for code in enabled_languages:
|
||||
TableSettingsLanguages.update({
|
||||
TableSettingsLanguages.enabled: 1
|
||||
})\
|
||||
.where(TableSettingsLanguages.code2 == code)\
|
||||
.execute()
|
||||
event_stream("languages")
|
||||
|
||||
languages_profiles = request.form.get('languages-profiles')
|
||||
if languages_profiles:
|
||||
existing_ids = TableLanguagesProfiles.select(TableLanguagesProfiles.profileId).dicts()
|
||||
existing_ids = list(existing_ids)
|
||||
existing = [x['profileId'] for x in existing_ids]
|
||||
for item in json.loads(languages_profiles):
|
||||
if item['profileId'] in existing:
|
||||
# Update existing profiles
|
||||
TableLanguagesProfiles.update({
|
||||
TableLanguagesProfiles.name: item['name'],
|
||||
TableLanguagesProfiles.cutoff: item['cutoff'] if item['cutoff'] != 'null' else None,
|
||||
TableLanguagesProfiles.items: json.dumps(item['items'])
|
||||
})\
|
||||
.where(TableLanguagesProfiles.profileId == item['profileId'])\
|
||||
.execute()
|
||||
existing.remove(item['profileId'])
|
||||
else:
|
||||
# Add new profiles
|
||||
TableLanguagesProfiles.insert({
|
||||
TableLanguagesProfiles.profileId: item['profileId'],
|
||||
TableLanguagesProfiles.name: item['name'],
|
||||
TableLanguagesProfiles.cutoff: item['cutoff'] if item['cutoff'] != 'null' else None,
|
||||
TableLanguagesProfiles.items: json.dumps(item['items'])
|
||||
}).execute()
|
||||
for profileId in existing:
|
||||
# Unassign this profileId from series and movies
|
||||
TableShows.update({
|
||||
TableShows.profileId: None
|
||||
}).where(TableShows.profileId == profileId).execute()
|
||||
TableMovies.update({
|
||||
TableMovies.profileId: None
|
||||
}).where(TableMovies.profileId == profileId).execute()
|
||||
# Remove deleted profiles
|
||||
TableLanguagesProfiles.delete().where(TableLanguagesProfiles.profileId == profileId).execute()
|
||||
|
||||
update_profile_id_list()
|
||||
event_stream("languages")
|
||||
|
||||
if settings.general.getboolean('use_sonarr'):
|
||||
scheduler.add_job(list_missing_subtitles, kwargs={'send_event': False})
|
||||
if settings.general.getboolean('use_radarr'):
|
||||
scheduler.add_job(list_missing_subtitles_movies, kwargs={'send_event': False})
|
||||
|
||||
# Update Notification
|
||||
notifications = request.form.getlist('notifications-providers')
|
||||
for item in notifications:
|
||||
item = json.loads(item)
|
||||
TableSettingsNotifier.update({
|
||||
TableSettingsNotifier.enabled: item['enabled'],
|
||||
TableSettingsNotifier.url: item['url']
|
||||
}).where(TableSettingsNotifier.name == item['name']).execute()
|
||||
|
||||
save_settings(zip(request.form.keys(), request.form.listvalues()))
|
||||
event_stream("settings")
|
||||
return '', 204
|
@ -0,0 +1,27 @@
|
||||
# coding=utf-8
|
||||
|
||||
import os
|
||||
import platform
|
||||
|
||||
from flask import jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from ..utils import authenticate
|
||||
from utils import get_sonarr_info, get_radarr_info
|
||||
from get_args import args
|
||||
from init import startTime
|
||||
|
||||
|
||||
class SystemStatus(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
system_status = {}
|
||||
system_status.update({'bazarr_version': os.environ["BAZARR_VERSION"]})
|
||||
system_status.update({'sonarr_version': get_sonarr_info.version()})
|
||||
system_status.update({'radarr_version': get_radarr_info.version()})
|
||||
system_status.update({'operating_system': platform.platform()})
|
||||
system_status.update({'python_version': platform.python_version()})
|
||||
system_status.update({'bazarr_directory': os.path.dirname(os.path.dirname(__file__))})
|
||||
system_status.update({'bazarr_config_directory': args.config_dir})
|
||||
system_status.update({'start_time': startTime})
|
||||
return jsonify(data=system_status)
|
@ -0,0 +1,18 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
class System(Resource):
|
||||
@authenticate
|
||||
def post(self):
|
||||
from server import webserver
|
||||
action = request.args.get('action')
|
||||
if action == "shutdown":
|
||||
webserver.shutdown()
|
||||
elif action == "restart":
|
||||
webserver.restart()
|
||||
return '', 204
|
@ -0,0 +1,31 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import request, jsonify
|
||||
from flask_restful import Resource
|
||||
|
||||
from ..utils import authenticate
|
||||
from scheduler import scheduler
|
||||
|
||||
|
||||
class SystemTasks(Resource):
|
||||
@authenticate
|
||||
def get(self):
|
||||
taskid = request.args.get('taskid')
|
||||
|
||||
task_list = scheduler.get_task_list()
|
||||
|
||||
if taskid:
|
||||
for item in task_list:
|
||||
if item['job_id'] == taskid:
|
||||
task_list = [item]
|
||||
continue
|
||||
|
||||
return jsonify(data=task_list)
|
||||
|
||||
@authenticate
|
||||
def post(self):
|
||||
taskid = request.form.get('taskid')
|
||||
|
||||
scheduler.execute_job_now(taskid)
|
||||
|
||||
return '', 204
|
@ -0,0 +1,239 @@
|
||||
# coding=utf-8
|
||||
|
||||
import ast
|
||||
|
||||
from functools import wraps
|
||||
from flask import request, abort
|
||||
from operator import itemgetter
|
||||
|
||||
from config import settings, base_url
|
||||
from get_languages import language_from_alpha2, alpha3_from_alpha2
|
||||
from database import get_audio_profile_languages, get_desired_languages
|
||||
from helper import path_mappings
|
||||
|
||||
None_Keys = ['null', 'undefined', '', None]
|
||||
|
||||
False_Keys = ['False', 'false', '0']
|
||||
|
||||
|
||||
def authenticate(actual_method):
|
||||
@wraps(actual_method)
|
||||
def wrapper(*args, **kwargs):
|
||||
apikey_settings = settings.auth.apikey
|
||||
apikey_get = request.args.get('apikey')
|
||||
apikey_post = request.form.get('apikey')
|
||||
apikey_header = None
|
||||
if 'X-API-KEY' in request.headers:
|
||||
apikey_header = request.headers['X-API-KEY']
|
||||
|
||||
if apikey_settings in [apikey_get, apikey_post, apikey_header]:
|
||||
return actual_method(*args, **kwargs)
|
||||
|
||||
return abort(401)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def postprocess(item):
|
||||
# Remove ffprobe_cache
|
||||
if 'ffprobe_cache' in item:
|
||||
del (item['ffprobe_cache'])
|
||||
|
||||
# Parse tags
|
||||
if 'tags' in item:
|
||||
if item['tags'] is None:
|
||||
item['tags'] = []
|
||||
else:
|
||||
item['tags'] = ast.literal_eval(item['tags'])
|
||||
|
||||
if 'monitored' in item:
|
||||
if item['monitored'] is None:
|
||||
item['monitored'] = False
|
||||
else:
|
||||
item['monitored'] = item['monitored'] == 'True'
|
||||
|
||||
if 'hearing_impaired' in item and item['hearing_impaired'] is not None:
|
||||
if item['hearing_impaired'] is None:
|
||||
item['hearing_impaired'] = False
|
||||
else:
|
||||
item['hearing_impaired'] = item['hearing_impaired'] == 'True'
|
||||
|
||||
if 'language' in item:
|
||||
if item['language'] == 'None':
|
||||
item['language'] = None
|
||||
elif item['language'] is not None:
|
||||
splitted_language = item['language'].split(':')
|
||||
item['language'] = {"name": language_from_alpha2(splitted_language[0]),
|
||||
"code2": splitted_language[0],
|
||||
"code3": alpha3_from_alpha2(splitted_language[0]),
|
||||
"forced": True if item['language'].endswith(':forced') else False,
|
||||
"hi": True if item['language'].endswith(':hi') else False}
|
||||
|
||||
|
||||
def postprocessSeries(item):
|
||||
postprocess(item)
|
||||
# Parse audio language
|
||||
if 'audio_language' in item and item['audio_language'] is not None:
|
||||
item['audio_language'] = get_audio_profile_languages(series_id=item['sonarrSeriesId'])
|
||||
|
||||
if 'alternateTitles' in item:
|
||||
if item['alternateTitles'] is None:
|
||||
item['alternativeTitles'] = []
|
||||
else:
|
||||
item['alternativeTitles'] = ast.literal_eval(item['alternateTitles'])
|
||||
del item["alternateTitles"]
|
||||
|
||||
# Parse seriesType
|
||||
if 'seriesType' in item and item['seriesType'] is not None:
|
||||
item['seriesType'] = item['seriesType'].capitalize()
|
||||
|
||||
if 'path' in item:
|
||||
item['path'] = path_mappings.path_replace(item['path'])
|
||||
|
||||
# map poster and fanart to server proxy
|
||||
if 'poster' in item:
|
||||
poster = item['poster']
|
||||
item['poster'] = f"{base_url}/images/series{poster}" if poster else None
|
||||
|
||||
if 'fanart' in item:
|
||||
fanart = item['fanart']
|
||||
item['fanart'] = f"{base_url}/images/series{fanart}" if fanart else None
|
||||
|
||||
|
||||
def postprocessEpisode(item):
|
||||
postprocess(item)
|
||||
if 'audio_language' in item and item['audio_language'] is not None:
|
||||
item['audio_language'] = get_audio_profile_languages(episode_id=item['sonarrEpisodeId'])
|
||||
|
||||
if 'subtitles' in item:
|
||||
if item['subtitles'] is None:
|
||||
raw_subtitles = []
|
||||
else:
|
||||
raw_subtitles = ast.literal_eval(item['subtitles'])
|
||||
subtitles = []
|
||||
|
||||
for subs in raw_subtitles:
|
||||
subtitle = subs[0].split(':')
|
||||
sub = {"name": language_from_alpha2(subtitle[0]),
|
||||
"code2": subtitle[0],
|
||||
"code3": alpha3_from_alpha2(subtitle[0]),
|
||||
"path": path_mappings.path_replace(subs[1]),
|
||||
"forced": False,
|
||||
"hi": False}
|
||||
if len(subtitle) > 1:
|
||||
sub["forced"] = True if subtitle[1] == 'forced' else False
|
||||
sub["hi"] = True if subtitle[1] == 'hi' else False
|
||||
|
||||
subtitles.append(sub)
|
||||
|
||||
item.update({"subtitles": subtitles})
|
||||
|
||||
# Parse missing subtitles
|
||||
if 'missing_subtitles' in item:
|
||||
if item['missing_subtitles'] is None:
|
||||
item['missing_subtitles'] = []
|
||||
else:
|
||||
item['missing_subtitles'] = ast.literal_eval(item['missing_subtitles'])
|
||||
for i, subs in enumerate(item['missing_subtitles']):
|
||||
subtitle = subs.split(':')
|
||||
item['missing_subtitles'][i] = {"name": language_from_alpha2(subtitle[0]),
|
||||
"code2": subtitle[0],
|
||||
"code3": alpha3_from_alpha2(subtitle[0]),
|
||||
"forced": False,
|
||||
"hi": False}
|
||||
if len(subtitle) > 1:
|
||||
item['missing_subtitles'][i].update({
|
||||
"forced": True if subtitle[1] == 'forced' else False,
|
||||
"hi": True if subtitle[1] == 'hi' else False
|
||||
})
|
||||
|
||||
if 'scene_name' in item:
|
||||
item["sceneName"] = item["scene_name"]
|
||||
del item["scene_name"]
|
||||
|
||||
if 'path' in item and item['path']:
|
||||
# Provide mapped path
|
||||
item['path'] = path_mappings.path_replace(item['path'])
|
||||
|
||||
|
||||
# TODO: Move
|
||||
def postprocessMovie(item):
|
||||
postprocess(item)
|
||||
# Parse audio language
|
||||
if 'audio_language' in item and item['audio_language'] is not None:
|
||||
item['audio_language'] = get_audio_profile_languages(movie_id=item['radarrId'])
|
||||
|
||||
# Parse alternate titles
|
||||
if 'alternativeTitles' in item:
|
||||
if item['alternativeTitles'] is None:
|
||||
item['alternativeTitles'] = []
|
||||
else:
|
||||
item['alternativeTitles'] = ast.literal_eval(item['alternativeTitles'])
|
||||
|
||||
# Parse failed attempts
|
||||
if 'failedAttempts' in item:
|
||||
if item['failedAttempts']:
|
||||
item['failedAttempts'] = ast.literal_eval(item['failedAttempts'])
|
||||
|
||||
# Parse subtitles
|
||||
if 'subtitles' in item:
|
||||
if item['subtitles'] is None:
|
||||
item['subtitles'] = []
|
||||
else:
|
||||
item['subtitles'] = ast.literal_eval(item['subtitles'])
|
||||
for i, subs in enumerate(item['subtitles']):
|
||||
language = subs[0].split(':')
|
||||
item['subtitles'][i] = {"path": path_mappings.path_replace_movie(subs[1]),
|
||||
"name": language_from_alpha2(language[0]),
|
||||
"code2": language[0],
|
||||
"code3": alpha3_from_alpha2(language[0]),
|
||||
"forced": False,
|
||||
"hi": False}
|
||||
if len(language) > 1:
|
||||
item['subtitles'][i].update({
|
||||
"forced": True if language[1] == 'forced' else False,
|
||||
"hi": True if language[1] == 'hi' else False
|
||||
})
|
||||
|
||||
if settings.general.getboolean('embedded_subs_show_desired'):
|
||||
desired_lang_list = get_desired_languages(item['profileId'])
|
||||
item['subtitles'] = [x for x in item['subtitles'] if x['code2'] in desired_lang_list or x['path']]
|
||||
|
||||
item['subtitles'] = sorted(item['subtitles'], key=itemgetter('name', 'forced'))
|
||||
|
||||
# Parse missing subtitles
|
||||
if 'missing_subtitles' in item:
|
||||
if item['missing_subtitles'] is None:
|
||||
item['missing_subtitles'] = []
|
||||
else:
|
||||
item['missing_subtitles'] = ast.literal_eval(item['missing_subtitles'])
|
||||
for i, subs in enumerate(item['missing_subtitles']):
|
||||
language = subs.split(':')
|
||||
item['missing_subtitles'][i] = {"name": language_from_alpha2(language[0]),
|
||||
"code2": language[0],
|
||||
"code3": alpha3_from_alpha2(language[0]),
|
||||
"forced": False,
|
||||
"hi": False}
|
||||
if len(language) > 1:
|
||||
item['missing_subtitles'][i].update({
|
||||
"forced": True if language[1] == 'forced' else False,
|
||||
"hi": True if language[1] == 'hi' else False
|
||||
})
|
||||
|
||||
# Provide mapped path
|
||||
if 'path' in item:
|
||||
if item['path']:
|
||||
item['path'] = path_mappings.path_replace_movie(item['path'])
|
||||
|
||||
if 'subtitles_path' in item:
|
||||
# Provide mapped subtitles path
|
||||
item['subtitles_path'] = path_mappings.path_replace_movie(item['subtitles_path'])
|
||||
|
||||
# map poster and fanart to server proxy
|
||||
if 'poster' in item:
|
||||
poster = item['poster']
|
||||
item['poster'] = f"{base_url}/images/movies{poster}" if poster else None
|
||||
|
||||
if 'fanart' in item:
|
||||
fanart = item['fanart']
|
||||
item['fanart'] = f"{base_url}/images/movies{fanart}" if fanart else None
|
@ -0,0 +1,12 @@
|
||||
# coding=utf-8
|
||||
|
||||
from flask import Blueprint
|
||||
from flask_restful import Api
|
||||
|
||||
from .plex import WebHooksPlex
|
||||
|
||||
|
||||
api_bp_webhooks = Blueprint('api_webhooks', __name__)
|
||||
api = Api(api_bp_webhooks)
|
||||
|
||||
api.add_resource(WebHooksPlex, '/webhooks/plex')
|
@ -0,0 +1,76 @@
|
||||
# coding=utf-8
|
||||
|
||||
import json
|
||||
import requests
|
||||
import os
|
||||
import re
|
||||
|
||||
from flask import request
|
||||
from flask_restful import Resource
|
||||
from bs4 import BeautifulSoup as bso
|
||||
|
||||
from database import TableEpisodes, TableShows, TableMovies
|
||||
from get_subtitle import episode_download_subtitles, movies_download_subtitles
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
class WebHooksPlex(Resource):
|
||||
@authenticate
|
||||
def post(self):
|
||||
json_webhook = request.form.get('payload')
|
||||
parsed_json_webhook = json.loads(json_webhook)
|
||||
|
||||
event = parsed_json_webhook['event']
|
||||
if event not in ['media.play']:
|
||||
return '', 204
|
||||
|
||||
media_type = parsed_json_webhook['Metadata']['type']
|
||||
|
||||
if media_type == 'episode':
|
||||
season = parsed_json_webhook['Metadata']['parentIndex']
|
||||
episode = parsed_json_webhook['Metadata']['index']
|
||||
else:
|
||||
season = episode = None
|
||||
|
||||
ids = []
|
||||
for item in parsed_json_webhook['Metadata']['Guid']:
|
||||
splitted_id = item['id'].split('://')
|
||||
if len(splitted_id) == 2:
|
||||
ids.append({splitted_id[0]: splitted_id[1]})
|
||||
if not ids:
|
||||
return '', 404
|
||||
|
||||
if media_type == 'episode':
|
||||
try:
|
||||
episode_imdb_id = [x['imdb'] for x in ids if 'imdb' in x][0]
|
||||
r = requests.get('https://imdb.com/title/{}'.format(episode_imdb_id),
|
||||
headers={"User-Agent": os.environ["SZ_USER_AGENT"]})
|
||||
soup = bso(r.content, "html.parser")
|
||||
series_imdb_id = soup.find('a', {'class': re.compile(r'SeriesParentLink__ParentTextLink')})['href'].split('/')[2]
|
||||
except:
|
||||
return '', 404
|
||||
else:
|
||||
sonarrEpisodeId = TableEpisodes.select(TableEpisodes.sonarrEpisodeId) \
|
||||
.join(TableShows, on=(TableEpisodes.sonarrSeriesId == TableShows.sonarrSeriesId)) \
|
||||
.where(TableShows.imdbId == series_imdb_id,
|
||||
TableEpisodes.season == season,
|
||||
TableEpisodes.episode == episode) \
|
||||
.dicts() \
|
||||
.get()
|
||||
|
||||
if sonarrEpisodeId:
|
||||
episode_download_subtitles(no=sonarrEpisodeId['sonarrEpisodeId'], send_progress=True)
|
||||
else:
|
||||
try:
|
||||
movie_imdb_id = [x['imdb'] for x in ids if 'imdb' in x][0]
|
||||
except:
|
||||
return '', 404
|
||||
else:
|
||||
radarrId = TableMovies.select(TableMovies.radarrId)\
|
||||
.where(TableMovies.imdbId == movie_imdb_id)\
|
||||
.dicts()\
|
||||
.get()
|
||||
if radarrId:
|
||||
movies_download_subtitles(no=radarrId['radarrId'])
|
||||
|
||||
return '', 200
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,63 @@
|
||||
from typing import Any, Dict, List, Iterable, Iterator, Optional
|
||||
|
||||
from . import (AppriseAsset, AppriseAttachment, AppriseConfig, ConfigBase,
|
||||
NotifyBase, NotifyFormat, NotifyType)
|
||||
from .common import ContentLocation
|
||||
|
||||
_Server = Union[str, ConfigBase, NotifyBase, AppriseConfig]
|
||||
_Servers = Union[_Server, Dict[Any, _Server], Iterable[_Server]]
|
||||
# Can't define this recursively as mypy doesn't support recursive types:
|
||||
# https://github.com/python/mypy/issues/731
|
||||
_Tag = Union[str, Iterable[Union[str, Iterable[str]]]]
|
||||
|
||||
class Apprise:
|
||||
def __init__(
|
||||
self,
|
||||
servers: _Servers = ...,
|
||||
asset: Optional[AppriseAsset] = ...,
|
||||
location: Optional[ContentLocation] = ...,
|
||||
debug: bool = ...
|
||||
) -> None: ...
|
||||
@staticmethod
|
||||
def instantiate(
|
||||
url: Union[str, Dict[str, NotifyBase]],
|
||||
asset: Optional[AppriseAsset] = ...,
|
||||
tag: Optional[_Tag] = ...,
|
||||
suppress_exceptions: bool = ...
|
||||
) -> NotifyBase: ...
|
||||
def add(
|
||||
self,
|
||||
servers: _Servers = ...,
|
||||
asset: Optional[AppriseAsset] = ...,
|
||||
tag: Optional[_Tag] = ...
|
||||
) -> bool: ...
|
||||
def clear(self) -> None: ...
|
||||
def find(self, tag: str = ...) -> Iterator[Apprise]: ...
|
||||
def notify(
|
||||
self,
|
||||
body: str,
|
||||
title: str = ...,
|
||||
notify_type: NotifyType = ...,
|
||||
body_format: NotifyFormat = ...,
|
||||
tag: _Tag = ...,
|
||||
attach: Optional[AppriseAttachment] = ...,
|
||||
interpret_escapes: Optional[bool] = ...
|
||||
) -> bool: ...
|
||||
async def async_notify(
|
||||
self,
|
||||
body: str,
|
||||
title: str = ...,
|
||||
notify_type: NotifyType = ...,
|
||||
body_format: NotifyFormat = ...,
|
||||
tag: _Tag = ...,
|
||||
attach: Optional[AppriseAttachment] = ...,
|
||||
interpret_escapes: Optional[bool] = ...
|
||||
) -> bool: ...
|
||||
def details(self, lang: Optional[str] = ...) -> Dict[str, Any]: ...
|
||||
def urls(self, privacy: bool = ...) -> Iterable[str]: ...
|
||||
def pop(self, index: int) -> ConfigBase: ...
|
||||
def __getitem__(self, index: int) -> ConfigBase: ...
|
||||
def __bool__(self) -> bool: ...
|
||||
def __nonzero__(self) -> bool: ...
|
||||
def __iter__(self) -> Iterator[ConfigBase]: ...
|
||||
def __len__(self) -> int: ...
|
@ -0,0 +1,34 @@
|
||||
from typing import Dict, Optional
|
||||
|
||||
from . import NotifyFormat, NotifyType
|
||||
|
||||
class AppriseAsset:
|
||||
app_id: str
|
||||
app_desc: str
|
||||
app_url: str
|
||||
html_notify_map: Dict[NotifyType, str]
|
||||
default_html_color: str
|
||||
default_extension: str
|
||||
theme: Optional[str]
|
||||
image_url_mask: str
|
||||
image_url_logo: str
|
||||
image_path_mask: Optional[str]
|
||||
body_format: Optional[NotifyFormat]
|
||||
async_mode: bool
|
||||
interpret_escapes: bool
|
||||
def __init__(
|
||||
self,
|
||||
app_id: str = ...,
|
||||
app_desc: str = ...,
|
||||
app_url: str = ...,
|
||||
html_notify_map: Dict[NotifyType, str] = ...,
|
||||
default_html_color: str = ...,
|
||||
default_extension: str = ...,
|
||||
theme: Optional[str] = ...,
|
||||
image_url_mask: str = ...,
|
||||
image_url_logo: str = ...,
|
||||
image_path_mask: Optional[str] = ...,
|
||||
body_format: Optional[NotifyFormat] = ...,
|
||||
async_mode: bool = ...,
|
||||
interpret_escapes: bool = ...
|
||||
) -> None: ...
|
@ -0,0 +1,38 @@
|
||||
from typing import Any, Iterable, Optional, Union
|
||||
|
||||
from . import AppriseAsset, ContentLocation
|
||||
from .attachment import AttachBase
|
||||
|
||||
_Attachment = Union[str, AttachBase]
|
||||
_Attachments = Iterable[_Attachment]
|
||||
|
||||
class AppriseAttachment:
|
||||
def __init__(
|
||||
self,
|
||||
paths: Optional[_Attachments] = ...,
|
||||
asset: Optional[AppriseAttachment] = ...,
|
||||
cache: bool = ...,
|
||||
location: Optional[ContentLocation] = ...,
|
||||
**kwargs: Any
|
||||
) -> None: ...
|
||||
def add(
|
||||
self,
|
||||
attachments: _Attachments,
|
||||
asset: Optional[AppriseAttachment] = ...,
|
||||
cache: Optional[bool] = ...
|
||||
) -> bool: ...
|
||||
@staticmethod
|
||||
def instantiate(
|
||||
url: str,
|
||||
asset: Optional[AppriseAsset] = ...,
|
||||
cache: Optional[bool] = ...,
|
||||
suppress_exceptions: bool = ...
|
||||
) -> NotifyBase: ...
|
||||
def clear(self) -> None: ...
|
||||
def size(self) -> int: ...
|
||||
def pop(self, index: int = ...) -> AttachBase: ...
|
||||
def __getitem__(self, index: int) -> AttachBase: ...
|
||||
def __bool__(self) -> bool: ...
|
||||
def __nonzero__(self) -> bool: ...
|
||||
def __iter__(self) -> Iterator[AttachBase]: ...
|
||||
def __len__(self) -> int: ...
|
@ -0,0 +1,49 @@
|
||||
from typing import Any, Iterable, Iterator, List, Optional, Union
|
||||
|
||||
from . import AppriseAsset, NotifyBase
|
||||
from .config import ConfigBase
|
||||
|
||||
_Configs = Union[ConfigBase, str, Iterable[str]]
|
||||
|
||||
class AppriseConfig:
|
||||
def __init__(
|
||||
self,
|
||||
paths: Optional[_Configs] = ...,
|
||||
asset: Optional[AppriseAsset] = ...,
|
||||
cache: bool = ...,
|
||||
recursion: int = ...,
|
||||
insecure_includes: bool = ...,
|
||||
**kwargs: Any
|
||||
) -> None: ...
|
||||
def add(
|
||||
self,
|
||||
configs: _Configs,
|
||||
asset: Optional[AppriseAsset] = ...,
|
||||
cache: bool = ...,
|
||||
recursion: Optional[bool] = ...,
|
||||
insecure_includes: Optional[bool] = ...
|
||||
) -> bool: ...
|
||||
def add_config(
|
||||
self,
|
||||
content: str,
|
||||
asset: Optional[AppriseAsset] = ...,
|
||||
tag: Optional[str] = ...,
|
||||
format: Optional[str] = ...,
|
||||
recursion: Optional[int] = ...,
|
||||
insecure_includes: Optional[bool] = ...
|
||||
) -> bool: ...
|
||||
def servers(self, tag: str = ..., *args: Any, **kwargs: Any) -> List[ConfigBase]: ...
|
||||
def instantiate(
|
||||
url: str,
|
||||
asset: Optional[AppriseAsset] = ...,
|
||||
tag: Optional[str] = ...,
|
||||
cache: Optional[bool] = ...
|
||||
) -> NotifyBase: ...
|
||||
def clear(self) -> None: ...
|
||||
def server_pop(self, index: int) -> ConfigBase: ...
|
||||
def pop(self, index: int = ...) -> ConfigBase: ...
|
||||
def __getitem__(self, index: int) -> ConfigBase: ...
|
||||
def __bool__(self) -> bool: ...
|
||||
def __nonzero__(self) -> bool: ...
|
||||
def __iter__(self) -> Iterator[ConfigBase]: ...
|
||||
def __len__(self) -> int: ...
|
@ -0,0 +1,16 @@
|
||||
from logging import logger
|
||||
from typing import Any, Iterable, Set, Optional
|
||||
|
||||
class URLBase:
|
||||
service_name: Optional[str]
|
||||
protocol: Optional[str]
|
||||
secure_protocol: Optional[str]
|
||||
request_rate_per_sec: int
|
||||
socket_connect_timeout: float
|
||||
socket_read_timeout: float
|
||||
tags: Set[str]
|
||||
verify_certificate: bool
|
||||
logger: logger
|
||||
def url(self, privacy: bool = ..., *args: Any, **kwargs: Any) -> str: ...
|
||||
def __contains__(self, tags: Iterable[str]) -> bool: ...
|
||||
def __str__(self) -> str: ...
|
@ -1,22 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:import namespace="http://schemas.xmlsoap.org/soap/envelope/" schemaLocation="http://schemas.xmlsoap.org/soap/envelope/"/>
|
||||
<xs:element name="Notification">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="Version" type="xs:string" />
|
||||
<xs:element name="MessageType" type="xs:string" />
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="success" />
|
||||
<xs:enumeration value="failure" />
|
||||
<xs:enumeration value="info" />
|
||||
<xs:enumeration value="warning" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:element>
|
||||
<xs:element name="Subject" type="xs:string" />
|
||||
<xs:element name="Message" type="xs:string" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="Version" type="xs:string" />
|
||||
<xs:element name="Subject" type="xs:string" />
|
||||
<xs:element name="MessageType">
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="success" />
|
||||
<xs:enumeration value="failure" />
|
||||
<xs:enumeration value="info" />
|
||||
<xs:enumeration value="warning" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:element>
|
||||
<xs:element name="Message" type="xs:string" />
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
||||
|
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<xs:import namespace="http://schemas.xmlsoap.org/soap/envelope/" schemaLocation="http://schemas.xmlsoap.org/soap/envelope/"/>
|
||||
<xs:element name="Notification">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="Version" type="xs:string" />
|
||||
<xs:element name="Subject" type="xs:string" />
|
||||
<xs:element name="MessageType">
|
||||
<xs:simpleType>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="success" />
|
||||
<xs:enumeration value="failure" />
|
||||
<xs:enumeration value="info" />
|
||||
<xs:enumeration value="warning" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
</xs:element>
|
||||
<xs:element name="Message" type="xs:string" />
|
||||
<xs:element name="Attachments" minOccurs="0">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="Attachment" minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:complexType>
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:string">
|
||||
<xs:attribute name="mimetype" type="xs:string" use="required"/>
|
||||
<xs:attribute name="filename" type="xs:string" use="required"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="encoding" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:sequence>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
@ -0,0 +1,37 @@
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from .. import ContentLocation
|
||||
|
||||
class AttachBase:
|
||||
max_detect_buffer_size: int
|
||||
unknown_mimetype: str
|
||||
unknown_filename: str
|
||||
unknown_filename_extension: str
|
||||
strict: bool
|
||||
max_file_size: int
|
||||
location: ContentLocation
|
||||
template_args: Dict[str, Any]
|
||||
def __init__(
|
||||
self,
|
||||
name: Optional[str] = ...,
|
||||
mimetype: Optional[str] = ...,
|
||||
cache: Optional[bool] = ...,
|
||||
**kwargs: Any
|
||||
) -> None: ...
|
||||
@property
|
||||
def path(self) -> Optional[str]: ...
|
||||
@property
|
||||
def name(self) -> Optional[str]: ...
|
||||
@property
|
||||
def mimetype(self) -> Optional[str]: ...
|
||||
def exists(self) -> bool: ...
|
||||
def invalidate(self) -> None: ...
|
||||
def download(self) -> bool: ...
|
||||
@staticmethod
|
||||
def parse_url(
|
||||
url: str,
|
||||
verify_host: bool = ...
|
||||
) -> Dict[str, Any]: ...
|
||||
def __len__(self) -> int: ...
|
||||
def __bool__(self) -> bool: ...
|
||||
def __nonzero__(self) -> bool: ...
|
@ -0,0 +1,15 @@
|
||||
class NotifyType:
|
||||
INFO: NotifyType
|
||||
SUCCESS: NotifyType
|
||||
WARNING: NotifyType
|
||||
FAILURE: NotifyType
|
||||
|
||||
class NotifyFormat:
|
||||
TEXT: NotifyFormat
|
||||
HTML: NotifyFormat
|
||||
MARKDOWN: NotifyFormat
|
||||
|
||||
class ContentLocation:
|
||||
LOCAL: ContentLocation
|
||||
HOSTED: ContentLocation
|
||||
INACCESSIBLE: ContentLocation
|
@ -0,0 +1,3 @@
|
||||
from .. import URLBase
|
||||
|
||||
class ConfigBase(URLBase): ...
|
@ -0,0 +1,382 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2021 Chris Caron <lead2gold@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# This code is licensed under the MIT License.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files(the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions :
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
import re
|
||||
import six
|
||||
import requests
|
||||
from json import dumps
|
||||
|
||||
from .NotifyBase import NotifyBase
|
||||
from ..URLBase import PrivacyMode
|
||||
from ..common import NotifyType
|
||||
from ..utils import parse_list
|
||||
from ..utils import validate_regex
|
||||
from ..AppriseLocale import gettext_lazy as _
|
||||
|
||||
|
||||
class NotifyAppriseAPI(NotifyBase):
|
||||
"""
|
||||
A wrapper for Apprise (Persistent) API Notifications
|
||||
"""
|
||||
|
||||
# The default descriptive name associated with the Notification
|
||||
service_name = 'Apprise API'
|
||||
|
||||
# The services URL
|
||||
service_url = 'https://github.com/caronc/apprise-api'
|
||||
|
||||
# The default protocol
|
||||
protocol = 'apprise'
|
||||
|
||||
# The default secure protocol
|
||||
secure_protocol = 'apprises'
|
||||
|
||||
# A URL that takes you to the setup/help of the specific protocol
|
||||
setup_url = 'https://github.com/caronc/apprise/wiki/Notify_apprise_api'
|
||||
|
||||
# Depending on the number of transactions/notifications taking place, this
|
||||
# could take a while. 30 seconds should be enough to perform the task
|
||||
socket_connect_timeout = 30.0
|
||||
|
||||
# Disable throttle rate for Apprise API requests since they are normally
|
||||
# local anyway
|
||||
request_rate_per_sec = 0.0
|
||||
|
||||
# Define object templates
|
||||
templates = (
|
||||
'{schema}://{host}/{token}',
|
||||
'{schema}://{host}:{port}/{token}',
|
||||
'{schema}://{user}@{host}/{token}',
|
||||
'{schema}://{user}@{host}:{port}/{token}',
|
||||
'{schema}://{user}:{password}@{host}/{token}',
|
||||
'{schema}://{user}:{password}@{host}:{port}/{token}',
|
||||
)
|
||||
|
||||
# Define our tokens; these are the minimum tokens required required to
|
||||
# be passed into this function (as arguments). The syntax appends any
|
||||
# previously defined in the base package and builds onto them
|
||||
template_tokens = dict(NotifyBase.template_tokens, **{
|
||||
'host': {
|
||||
'name': _('Hostname'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
},
|
||||
'port': {
|
||||
'name': _('Port'),
|
||||
'type': 'int',
|
||||
'min': 1,
|
||||
'max': 65535,
|
||||
},
|
||||
'user': {
|
||||
'name': _('Username'),
|
||||
'type': 'string',
|
||||
},
|
||||
'password': {
|
||||
'name': _('Password'),
|
||||
'type': 'string',
|
||||
'private': True,
|
||||
},
|
||||
'token': {
|
||||
'name': _('Token'),
|
||||
'type': 'string',
|
||||
'required': True,
|
||||
'private': True,
|
||||
'regex': (r'^[A-Z0-9_-]{1,32}$', 'i'),
|
||||
},
|
||||
})
|
||||
|
||||
# Define our template arguments
|
||||
template_args = dict(NotifyBase.template_args, **{
|
||||
'tags': {
|
||||
'name': _('Tags'),
|
||||
'type': 'string',
|
||||
},
|
||||
'to': {
|
||||
'alias_of': 'token',
|
||||
},
|
||||
})
|
||||
|
||||
# Define any kwargs we're using
|
||||
template_kwargs = {
|
||||
'headers': {
|
||||
'name': _('HTTP Header'),
|
||||
'prefix': '+',
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self, token=None, tags=None, headers=None, **kwargs):
|
||||
"""
|
||||
Initialize Apprise API Object
|
||||
|
||||
headers can be a dictionary of key/value pairs that you want to
|
||||
additionally include as part of the server headers to post with
|
||||
|
||||
"""
|
||||
super(NotifyAppriseAPI, self).__init__(**kwargs)
|
||||
|
||||
self.fullpath = kwargs.get('fullpath')
|
||||
if not isinstance(self.fullpath, six.string_types):
|
||||
self.fullpath = '/'
|
||||
|
||||
self.token = validate_regex(
|
||||
token, *self.template_tokens['token']['regex'])
|
||||
if not self.token:
|
||||
msg = 'The Apprise API token specified ({}) is invalid.'\
|
||||
.format(token)
|
||||
self.logger.warning(msg)
|
||||
raise TypeError(msg)
|
||||
|
||||
# Build list of tags
|
||||
self.__tags = parse_list(tags)
|
||||
|
||||
self.headers = {}
|
||||
if headers:
|
||||
# Store our extra headers
|
||||
self.headers.update(headers)
|
||||
|
||||
return
|
||||
|
||||
def url(self, privacy=False, *args, **kwargs):
|
||||
"""
|
||||
Returns the URL built dynamically based on specified arguments.
|
||||
"""
|
||||
|
||||
# Our URL parameters
|
||||
params = self.url_parameters(privacy=privacy, *args, **kwargs)
|
||||
|
||||
# Append our headers into our parameters
|
||||
params.update({'+{}'.format(k): v for k, v in self.headers.items()})
|
||||
|
||||
if self.__tags:
|
||||
params['tags'] = ','.join([x for x in self.__tags])
|
||||
|
||||
# Determine Authentication
|
||||
auth = ''
|
||||
if self.user and self.password:
|
||||
auth = '{user}:{password}@'.format(
|
||||
user=NotifyAppriseAPI.quote(self.user, safe=''),
|
||||
password=self.pprint(
|
||||
self.password, privacy, mode=PrivacyMode.Secret, safe=''),
|
||||
)
|
||||
elif self.user:
|
||||
auth = '{user}@'.format(
|
||||
user=NotifyAppriseAPI.quote(self.user, safe=''),
|
||||
)
|
||||
|
||||
default_port = 443 if self.secure else 80
|
||||
|
||||
fullpath = self.fullpath.strip('/')
|
||||
return '{schema}://{auth}{hostname}{port}{fullpath}{token}' \
|
||||
'/?{params}'.format(
|
||||
schema=self.secure_protocol
|
||||
if self.secure else self.protocol,
|
||||
auth=auth,
|
||||
# never encode hostname since we're expecting it to be a
|
||||
# valid one
|
||||
hostname=self.host,
|
||||
port='' if self.port is None or self.port == default_port
|
||||
else ':{}'.format(self.port),
|
||||
fullpath='/{}/'.format(NotifyAppriseAPI.quote(
|
||||
fullpath, safe='/')) if fullpath else '/',
|
||||
token=self.pprint(self.token, privacy, safe=''),
|
||||
params=NotifyAppriseAPI.urlencode(params))
|
||||
|
||||
def send(self, body, title='', notify_type=NotifyType.INFO, **kwargs):
|
||||
"""
|
||||
Perform Apprise API Notification
|
||||
"""
|
||||
|
||||
headers = {}
|
||||
# Apply any/all header over-rides defined
|
||||
headers.update(self.headers)
|
||||
|
||||
# prepare Apprise API Object
|
||||
payload = {
|
||||
# Apprise API Payload
|
||||
'title': title,
|
||||
'body': body,
|
||||
'type': notify_type,
|
||||
'format': self.notify_format,
|
||||
}
|
||||
|
||||
if self.__tags:
|
||||
payload['tag'] = self.__tags
|
||||
|
||||
auth = None
|
||||
if self.user:
|
||||
auth = (self.user, self.password)
|
||||
|
||||
# Set our schema
|
||||
schema = 'https' if self.secure else 'http'
|
||||
|
||||
url = '%s://%s' % (schema, self.host)
|
||||
if isinstance(self.port, int):
|
||||
url += ':%d' % self.port
|
||||
|
||||
fullpath = self.fullpath.strip('/')
|
||||
url += '/{}/'.format(fullpath) if fullpath else '/'
|
||||
url += 'notify/{}'.format(self.token)
|
||||
|
||||
# Some entries can not be over-ridden
|
||||
headers.update({
|
||||
'User-Agent': self.app_id,
|
||||
'Content-Type': 'application/json',
|
||||
# Pass our Source UUID4 Identifier
|
||||
'X-Apprise-ID': self.asset._uid,
|
||||
# Pass our current recursion count to our upstream server
|
||||
'X-Apprise-Recursion-Count': str(self.asset._recursion + 1),
|
||||
})
|
||||
|
||||
self.logger.debug('Apprise API POST URL: %s (cert_verify=%r)' % (
|
||||
url, self.verify_certificate,
|
||||
))
|
||||
self.logger.debug('Apprise API Payload: %s' % str(payload))
|
||||
|
||||
# Always call throttle before any remote server i/o is made
|
||||
self.throttle()
|
||||
|
||||
try:
|
||||
r = requests.post(
|
||||
url,
|
||||
data=dumps(payload),
|
||||
headers=headers,
|
||||
auth=auth,
|
||||
verify=self.verify_certificate,
|
||||
timeout=self.request_timeout,
|
||||
)
|
||||
if r.status_code != requests.codes.ok:
|
||||
# We had a problem
|
||||
status_str = \
|
||||
NotifyAppriseAPI.http_response_code_lookup(r.status_code)
|
||||
|
||||
self.logger.warning(
|
||||
'Failed to send Apprise API notification: '
|
||||
'{}{}error={}.'.format(
|
||||
status_str,
|
||||
', ' if status_str else '',
|
||||
r.status_code))
|
||||
|
||||
self.logger.debug('Response Details:\r\n{}'.format(r.content))
|
||||
|
||||
# Return; we're done
|
||||
return False
|
||||
|
||||
else:
|
||||
self.logger.info('Sent Apprise API notification.')
|
||||
|
||||
except requests.RequestException as e:
|
||||
self.logger.warning(
|
||||
'A Connection error occurred sending Apprise API '
|
||||
'notification to %s.' % self.host)
|
||||
self.logger.debug('Socket Exception: %s' % str(e))
|
||||
|
||||
# Return; we're done
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def parse_native_url(url):
|
||||
"""
|
||||
Support http://hostname/notify/token and
|
||||
http://hostname/path/notify/token
|
||||
"""
|
||||
|
||||
result = re.match(
|
||||
r'^http(?P<secure>s?)://(?P<hostname>[A-Z0-9._-]+)'
|
||||
r'(:(?P<port>[0-9]+))?'
|
||||
r'(?P<path>/[^?]+?)?/notify/(?P<token>[A-Z0-9_-]{1,32})/?'
|
||||
r'(?P<params>\?.+)?$', url, re.I)
|
||||
|
||||
if result:
|
||||
return NotifyAppriseAPI.parse_url(
|
||||
'{schema}://{hostname}{port}{path}/{token}/{params}'.format(
|
||||
schema=NotifyAppriseAPI.secure_protocol
|
||||
if result.group('secure') else NotifyAppriseAPI.protocol,
|
||||
hostname=result.group('hostname'),
|
||||
port='' if not result.group('port')
|
||||
else ':{}'.format(result.group('port')),
|
||||
path='' if not result.group('path')
|
||||
else result.group('path'),
|
||||
token=result.group('token'),
|
||||
params='' if not result.group('params')
|
||||
else '?{}'.format(result.group('params'))))
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def parse_url(url):
|
||||
"""
|
||||
Parses the URL and returns enough arguments that can allow
|
||||
us to re-instantiate this object.
|
||||
|
||||
"""
|
||||
results = NotifyBase.parse_url(url)
|
||||
if not results:
|
||||
# We're done early as we couldn't load the results
|
||||
return results
|
||||
|
||||
# Add our headers that the user can potentially over-ride if they wish
|
||||
# to to our returned result set
|
||||
results['headers'] = results['qsd+']
|
||||
if results['qsd-']:
|
||||
results['headers'].update(results['qsd-'])
|
||||
NotifyBase.logger.deprecate(
|
||||
"minus (-) based Apprise API header tokens are being "
|
||||
" removed; use the plus (+) symbol instead.")
|
||||
|
||||
# Tidy our header entries by unquoting them
|
||||
results['headers'] = \
|
||||
{NotifyAppriseAPI.unquote(x): NotifyAppriseAPI.unquote(y)
|
||||
for x, y in results['headers'].items()}
|
||||
|
||||
# Support the passing of tags in the URL
|
||||
if 'tags' in results['qsd'] and len(results['qsd']['tags']):
|
||||
results['tags'] = \
|
||||
NotifyAppriseAPI.parse_list(results['qsd']['tags'])
|
||||
|
||||
# Support the 'to' & 'token' variable so that we can support rooms
|
||||
# this way too.
|
||||
if 'token' in results['qsd'] and len(results['qsd']['token']):
|
||||
results['token'] = \
|
||||
NotifyAppriseAPI.unquote(results['qsd']['token'])
|
||||
|
||||
elif 'to' in results['qsd'] and len(results['qsd']['to']):
|
||||
results['token'] = NotifyAppriseAPI.unquote(results['qsd']['to'])
|
||||
|
||||
else:
|
||||
# Start with a list of path entries to work with
|
||||
entries = NotifyAppriseAPI.split_path(results['fullpath'])
|
||||
if entries:
|
||||
# use our last entry found
|
||||
results['token'] = entries[-1]
|
||||
|
||||
# pop our last entry off
|
||||
entries = entries[:-1]
|
||||
|
||||
# re-assemble our full path
|
||||
results['fullpath'] = '/'.join(entries)
|
||||
|
||||
return results
|
@ -0,0 +1 @@
|
||||
class NotifyBase: ...
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue