mirror of
https://github.com/morpheus65535/bazarr.git
synced 2025-12-23 23:59:04 -05:00
Refactored Sonarr and Radarr hook. It may be a breaking change so users should review webhook parameters following information in Bazarr's settings.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
# coding=utf-8
|
||||
import logging
|
||||
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
from flask_restx import Resource, Namespace, fields
|
||||
|
||||
from app.database import TableMovies, database, select
|
||||
from radarr.sync.movies import update_one_movie
|
||||
from subtitles.mass_download import movies_download_subtitles
|
||||
from subtitles.indexer.movies import store_subtitles_movie
|
||||
from utilities.path_mappings import path_mappings
|
||||
@@ -10,31 +12,99 @@ from utilities.path_mappings import path_mappings
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
api_ns_webhooks_radarr = Namespace('Webhooks Radarr', description='Webhooks to trigger subtitles search based on '
|
||||
'Radarr movie file ID')
|
||||
api_ns_webhooks_radarr = Namespace(
|
||||
"Webhooks Radarr",
|
||||
description="Webhooks to trigger subtitles search based on Radarr webhooks",
|
||||
)
|
||||
|
||||
|
||||
@api_ns_webhooks_radarr.route('webhooks/radarr')
|
||||
@api_ns_webhooks_radarr.route("webhooks/radarr")
|
||||
class WebHooksRadarr(Resource):
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('radarr_moviefile_id', type=int, required=True, help='Movie file ID')
|
||||
movie_model = api_ns_webhooks_radarr.model(
|
||||
"RadarrMovie",
|
||||
{
|
||||
"id": fields.Integer(required=True, description="Movie ID"),
|
||||
},
|
||||
strict=False,
|
||||
)
|
||||
|
||||
movie_file_model = api_ns_webhooks_radarr.model(
|
||||
"RadarrMovieFile",
|
||||
{
|
||||
"id": fields.Integer(required=True, description="Movie file ID"),
|
||||
},
|
||||
strict=False,
|
||||
)
|
||||
|
||||
radarr_webhook_model = api_ns_webhooks_radarr.model(
|
||||
"RadarrWebhook",
|
||||
{
|
||||
"eventType": fields.String(
|
||||
required=True,
|
||||
description="Type of Radarr event (e.g. MovieAdded, Test, etc)",
|
||||
),
|
||||
"movieFile": fields.Nested(
|
||||
movie_file_model,
|
||||
required=False,
|
||||
description="Radarr movie file payload. Required for anything other than test hooks",
|
||||
),
|
||||
"movie": fields.Nested(
|
||||
movie_model,
|
||||
required=False,
|
||||
description="Radarr movie payload. Can be used to sync movies from Radarr if not found in Bazarr",
|
||||
),
|
||||
},
|
||||
strict=False,
|
||||
)
|
||||
|
||||
@authenticate
|
||||
@api_ns_webhooks_radarr.doc(parser=post_request_parser)
|
||||
@api_ns_webhooks_radarr.response(200, 'Success')
|
||||
@api_ns_webhooks_radarr.response(401, 'Not Authenticated')
|
||||
@api_ns_webhooks_radarr.expect(radarr_webhook_model, validate=True)
|
||||
@api_ns_webhooks_radarr.response(200, "Success")
|
||||
@api_ns_webhooks_radarr.response(401, "Not Authenticated")
|
||||
def post(self):
|
||||
"""Search for missing subtitles for a specific movie file id"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
movie_file_id = args.get('radarr_moviefile_id')
|
||||
"""Search for missing subtitles based on Radarr webhooks"""
|
||||
args = api_ns_webhooks_radarr.payload
|
||||
event_type = args.get("eventType")
|
||||
|
||||
radarrMovieId = database.execute(
|
||||
logging.debug(f"Received Radarr webhook event: {event_type}")
|
||||
|
||||
if event_type == "Test":
|
||||
message = "Received test hook, skipping database search."
|
||||
logging.debug(message)
|
||||
return message, 200
|
||||
|
||||
movie_file_id = args.get("movieFile", {}).get("id")
|
||||
|
||||
if not movie_file_id:
|
||||
message = "No movie file ID found in the webhook request. Nothing to do."
|
||||
logging.debug(message)
|
||||
# Radarr reports the webhook as 'unhealthy' and requires
|
||||
# user interaction if we return anything except 200s.
|
||||
return message, 200
|
||||
|
||||
# This webhook is often faster than the database update,
|
||||
# so we update the movie first if we can.
|
||||
radarr_id = args.get("movie", {}).get("id")
|
||||
|
||||
q = (
|
||||
select(TableMovies.radarrId, TableMovies.path)
|
||||
.where(TableMovies.movie_file_id == movie_file_id)) \
|
||||
.where(TableMovies.movie_file_id == movie_file_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
if radarrMovieId:
|
||||
store_subtitles_movie(radarrMovieId.path, path_mappings.path_replace_movie(radarrMovieId.path))
|
||||
movies_download_subtitles(no=radarrMovieId.radarrId)
|
||||
movie = database.execute(q)
|
||||
if not movie and radarr_id:
|
||||
logging.debug(
|
||||
f"No movie matching file ID {movie_file_id} found in the database. Attempting to sync from Radarr."
|
||||
)
|
||||
update_one_movie(radarr_id, "updated")
|
||||
movie = database.execute(q)
|
||||
if not movie:
|
||||
message = f"No movie matching file ID {movie_file_id} found in the database. Nothing to do."
|
||||
logging.debug(message)
|
||||
return message, 200
|
||||
|
||||
return '', 200
|
||||
store_subtitles_movie(movie.path, path_mappings.path_replace_movie(movie.path))
|
||||
movies_download_subtitles(no=movie.radarrId)
|
||||
|
||||
return "Finished processing subtitles.", 200
|
||||
|
||||
@@ -1,42 +1,121 @@
|
||||
# coding=utf-8
|
||||
import logging
|
||||
|
||||
from flask_restx import Resource, Namespace, reqparse
|
||||
from flask_restx import Resource, Namespace, fields
|
||||
|
||||
from app.database import TableEpisodes, TableShows, database, select
|
||||
from sonarr.sync.episodes import sync_one_episode
|
||||
from subtitles.mass_download import episode_download_subtitles
|
||||
from subtitles.indexer.series import store_subtitles
|
||||
from utilities.path_mappings import path_mappings
|
||||
|
||||
|
||||
from ..utils import authenticate
|
||||
|
||||
|
||||
api_ns_webhooks_sonarr = Namespace('Webhooks Sonarr', description='Webhooks to trigger subtitles search based on '
|
||||
'Sonarr episode file ID')
|
||||
api_ns_webhooks_sonarr = Namespace(
|
||||
"Webhooks Sonarr",
|
||||
description="Webhooks to trigger subtitles search based on Sonarr webhooks",
|
||||
)
|
||||
|
||||
|
||||
@api_ns_webhooks_sonarr.route('webhooks/sonarr')
|
||||
@api_ns_webhooks_sonarr.route("webhooks/sonarr")
|
||||
class WebHooksSonarr(Resource):
|
||||
post_request_parser = reqparse.RequestParser()
|
||||
post_request_parser.add_argument('sonarr_episodefile_id', type=int, required=True, help='Episode file ID')
|
||||
episode_model = api_ns_webhooks_sonarr.model(
|
||||
"SonarrEpisode",
|
||||
{
|
||||
"id": fields.Integer(required=True, description="Episode ID"),
|
||||
},
|
||||
strict=False,
|
||||
)
|
||||
|
||||
episode_file_model = api_ns_webhooks_sonarr.model(
|
||||
"SonarrEpisodeFile",
|
||||
{
|
||||
"id": fields.Integer(required=True, description="Episode file ID"),
|
||||
},
|
||||
strict=False,
|
||||
)
|
||||
|
||||
sonarr_webhook_model = api_ns_webhooks_sonarr.model(
|
||||
"SonarrWebhook",
|
||||
{
|
||||
"episodes": fields.List(
|
||||
fields.Nested(episode_model),
|
||||
required=False,
|
||||
description="List of episodes. Can be used to sync episodes from Sonarr if not found in Bazarr.",
|
||||
),
|
||||
"episodeFiles": fields.List(
|
||||
fields.Nested(episode_file_model),
|
||||
required=False,
|
||||
description="List of episode files; required for anything other than test hooks",
|
||||
),
|
||||
"eventType": fields.String(
|
||||
required=True,
|
||||
description="Type of Sonarr event (e.g. Test, Download, etc.)",
|
||||
),
|
||||
},
|
||||
strict=False,
|
||||
)
|
||||
|
||||
@authenticate
|
||||
@api_ns_webhooks_sonarr.doc(parser=post_request_parser)
|
||||
@api_ns_webhooks_sonarr.response(200, 'Success')
|
||||
@api_ns_webhooks_sonarr.response(401, 'Not Authenticated')
|
||||
@api_ns_webhooks_sonarr.expect(sonarr_webhook_model, validate=True)
|
||||
@api_ns_webhooks_sonarr.response(200, "Success")
|
||||
@api_ns_webhooks_sonarr.response(401, "Not Authenticated")
|
||||
def post(self):
|
||||
"""Search for missing subtitles for a specific episode file id"""
|
||||
args = self.post_request_parser.parse_args()
|
||||
episode_file_id = args.get('sonarr_episodefile_id')
|
||||
"""Search for missing subtitles based on Sonarr webhooks"""
|
||||
args = api_ns_webhooks_sonarr.payload
|
||||
event_type = args.get("eventType")
|
||||
|
||||
sonarrEpisodeId = database.execute(
|
||||
select(TableEpisodes.sonarrEpisodeId, TableEpisodes.path)
|
||||
.select_from(TableEpisodes)
|
||||
.join(TableShows)
|
||||
.where(TableEpisodes.episode_file_id == episode_file_id)) \
|
||||
.first()
|
||||
logging.debug(f"Received Sonarr webhook event: {event_type}")
|
||||
|
||||
if sonarrEpisodeId:
|
||||
store_subtitles(sonarrEpisodeId.path, path_mappings.path_replace(sonarrEpisodeId.path))
|
||||
episode_download_subtitles(no=sonarrEpisodeId.sonarrEpisodeId, send_progress=True)
|
||||
if event_type == "Test":
|
||||
message = "Received test hook, skipping database search."
|
||||
logging.debug(message)
|
||||
return message, 200
|
||||
|
||||
return '', 200
|
||||
# Sonarr hooks only differentiate a download starting vs. ending by
|
||||
# the inclusion of episodeFiles in the payload.
|
||||
sonarr_episode_file_ids = [e.get("id") for e in args.get("episodeFiles", [])]
|
||||
|
||||
if not sonarr_episode_file_ids:
|
||||
message = "No episode file IDs found in the webhook request. Nothing to do."
|
||||
logging.debug(message)
|
||||
# Sonarr reports the webhook as 'unhealthy' and requires
|
||||
# user interaction if we return anything except 200s.
|
||||
return message, 200
|
||||
|
||||
sonarr_episode_ids = [e.get("id") for e in args.get("episodes", [])]
|
||||
|
||||
if len(sonarr_episode_ids) != len(sonarr_episode_file_ids):
|
||||
logging.debug(
|
||||
"Episode IDs and episode file IDs are different lengths, ignoring episode IDs."
|
||||
)
|
||||
sonarr_episode_ids = []
|
||||
|
||||
for i, efid in enumerate(sonarr_episode_file_ids):
|
||||
q = (
|
||||
select(TableEpisodes.sonarrEpisodeId, TableEpisodes.path)
|
||||
.select_from(TableEpisodes)
|
||||
.join(TableShows)
|
||||
.where(TableEpisodes.episode_file_id == efid)
|
||||
)
|
||||
|
||||
episode = database.execute(q).first()
|
||||
if not episode and sonarr_episode_ids:
|
||||
logging.debug(
|
||||
"No episode found for episode file ID %s, attempting to sync from Sonarr.",
|
||||
efid,
|
||||
)
|
||||
sync_one_episode(sonarr_episode_ids[i])
|
||||
episode = database.execute(q).first()
|
||||
if not episode:
|
||||
logging.debug(
|
||||
"No episode found for episode file ID %s, skipping.", efid
|
||||
)
|
||||
continue
|
||||
|
||||
store_subtitles(episode.path, path_mappings.path_replace(episode.path))
|
||||
episode_download_subtitles(no=episode.sonarrEpisodeId, send_progress=True)
|
||||
|
||||
return "Finished processing subtitles.", 200
|
||||
|
||||
@@ -84,9 +84,10 @@ const SettingsRadarrView: FunctionComponent = () => {
|
||||
<Message>
|
||||
Search can be triggered using this command
|
||||
<Code>
|
||||
curl -d "radarr_moviefile_id=$radarr_moviefile_id" -H "x-api-key:
|
||||
###############################" -X POST
|
||||
http://localhost:6767/api/webhooks/radarr
|
||||
{`curl -H "Content-Type: application/json" -H "X-API-KEY: ###############################" -X POST
|
||||
-d '{ "eventType": "Download", "movieFile": [ { "id": "$radarr_moviefile_id" } ] }'
|
||||
http://localhost:6767/api/webhooks/radarr
|
||||
`}
|
||||
</Code>
|
||||
</Message>
|
||||
</Section>
|
||||
|
||||
@@ -93,11 +93,12 @@ const SettingsSonarrView: FunctionComponent = () => {
|
||||
as soon as episodes are imported.
|
||||
</Message>
|
||||
<Message>
|
||||
Search can be triggered using this command
|
||||
Search can be triggered using this command:
|
||||
<Code>
|
||||
curl -d "sonarr_episodefile_id=$sonarr_episodefile_id" -H
|
||||
"x-api-key: ###############################" -X POST
|
||||
http://localhost:6767/api/webhooks/sonarr
|
||||
{`curl -H "Content-Type: application/json" -H "X-API-KEY: ###############################" -X POST
|
||||
-d '{ "eventType": "Download", "episodeFiles": [ { "id": "$sonarr_episodefile_id" } ] }'
|
||||
http://localhost:6767/api/webhooks/sonarr
|
||||
`}
|
||||
</Code>
|
||||
</Message>
|
||||
<Check
|
||||
|
||||
Reference in New Issue
Block a user