__init__.py 26.9 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# ##############################################################################
#  Author: echel0n <[email protected]>
#  URL: https://sickrage.ca/
#  Git: https://git.sickrage.ca/SiCKRAGE/sickrage.git
#  -
#  This file is part of SiCKRAGE.
#  -
#  SiCKRAGE is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#  -
#  SiCKRAGE is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  -
#  You should have received a copy of the GNU General Public License
#  along with SiCKRAGE.  If not, see <http://www.gnu.org/licenses/>.
# ##############################################################################
import os
import re
23
import time
24
from itertools import zip_longest
25
26
27
28

from tornado.escape import json_decode

import sickrage
29
from sickrage.core.common import Quality, Qualities, EpisodeStatus
30
from sickrage.core.databases.main import MainDB
31
from sickrage.core.databases.main.schemas import IMDbInfoSchema, BlacklistSchema, WhitelistSchema, TVShowSchema
32
from sickrage.core.enums import SearchFormat, SeriesProviderID
33
34
35
from sickrage.core.exceptions import CantUpdateShowException, NoNFOException, CantRefreshShowException
from sickrage.core.helpers import checkbox_to_value, sanitize_file_name, make_dir, chmod_as_parent
from sickrage.core.helpers.anidb import short_group_names
36
from sickrage.core.media.util import series_image, SeriesImageType
echel0n's avatar
echel0n committed
37
38
from sickrage.core.queues.search import ManualSearchTask
from sickrage.core.tv.episode.helpers import find_episode
39
from sickrage.core.tv.show.helpers import get_show_list, find_show, find_show_by_slug
40
from sickrage.core.webserver.handlers.api.v2 import ApiV2BaseHandler
echel0n's avatar
echel0n committed
41
from sickrage.core.websocket import WebSocketMessage
echel0n's avatar
echel0n committed
42
from .schemas import *
echel0n's avatar
echel0n committed
43
44


45
class ApiV2SeriesHandler(ApiV2BaseHandler):
echel0n's avatar
echel0n committed
46
47
48
49
50
51
52
53
54
    def get(self, series_slug=None):
        """Get list of series or specific series information"
        ---
        tags: [Series]
        summary: Manually search for episodes on search providers
        description: Manually search for episodes on search providers
        parameters:
        - in: path
          schema:
echel0n's avatar
echel0n committed
55
            SeriesSlugPath
echel0n's avatar
echel0n committed
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
        responses:
          200:
            description: Success payload
            content:
              application/json:
                schema:
                  SeriesSuccessSchema
          400:
            description: Bad request; Check `errors` for any validation errors
            content:
              application/json:
                schema:
                  BadRequestSchema
          401:
            description: Returned if your JWT token is missing or expired
            content:
              application/json:
                schema:
                  NotAuthorizedSchema
          404:
            description: Returned if the given series slug does not exist or no series results.
            content:
              application/json:
                schema:
                  NotFoundSchema
        """
82

83
84
85
        offset = int(self.get_argument('offset', 0))
        limit = int(self.get_argument('limit', 0))

86
        if not series_slug:
87
            all_series = []
88

89
90
91
92
93
94
95
96
97
98
99
100
101
102
            # start = time.time()

            with sickrage.app.main_db.session() as session:
                for series in session.query(MainDB.TVShow).with_entities(MainDB.TVShow.series_id, MainDB.TVShow.series_provider_id, MainDB.TVShow.name):
                    json_data = TVShowSchema().dump(series)
                    json_data['seriesSlug'] = f'{series.series_id}-{series.series_provider_id.value}'
                    json_data['isLoading'] = sickrage.app.show_queue.is_being_added(series.series_id)
                    json_data['isRemoving'] = sickrage.app.show_queue.is_being_removed(series.series_id)
                    json_data['images'] = {
                        'poster': series_image(series.series_id, series.series_provider_id, SeriesImageType.POSTER).url,
                        'banner': series_image(series.series_id, series.series_provider_id, SeriesImageType.BANNER).url
                    }

                    all_series.append(json_data)
103

104
105
                # end = time.time()
                # print(end - start)
106

107
108
109
110
111
112
113
114
115
116
            # start = time.time()
            #
            # for show in get_show_list(offset, limit):
            #     if show.is_removing:
            #         continue
            #
            #     all_series.append(show.to_json())
            #
            # end = time.time()
            # print(end - start)
117

118
            return self.json_response(all_series)
119

120
121
        series = find_show_by_slug(series_slug)
        if series is None:
122
            return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
123

124
        return self.json_response(series.to_json(episodes=True, details=True))
125
126
127
128

    def post(self):
        data = json_decode(self.request.body)

echel0n's avatar
echel0n committed
129
        is_existing = data.get('isExisting', 'false')
130
131
132
133

        root_directory = data.get('rootDirectory', None)
        series_id = data.get('seriesId', None)
        series_name = data.get('seriesName', None)
134
        series_directory = data.get('seriesDirectory', None)
135
        first_aired = data.get('firstAired', None)
136
        series_provider_slug = data.get('seriesProviderSlug', None)
echel0n's avatar
echel0n committed
137
        series_provider_language = data.get('seriesProviderLanguage', None)
138
139
140
        default_status = data.get('defaultStatus', None)
        default_status_after = data.get('defaultStatusAfter', None)
        quality_preset = data.get('qualityPreset', None)
141
142
        allowed_qualities = data.get('allowedQualities', [])
        preferred_qualities = data.get('preferredQualities', [])
echel0n's avatar
echel0n committed
143
144
145
146
147
148
149
150
151
        subtitles = self._parse_boolean(data.get('subtitles', sickrage.app.config.subtitles.default))
        sub_use_sr_metadata = self._parse_boolean(data.get('subUseSrMetadata', 'false'))
        flatten_folders = self._parse_boolean(data.get('flattenFolders', sickrage.app.config.general.flatten_folders_default))
        is_anime = self._parse_boolean(data.get('isAnime', sickrage.app.config.general.anime_default))
        is_scene = self._parse_boolean(data.get('isScene', sickrage.app.config.general.scene_default))
        search_format = data.get('searchFormat', sickrage.app.config.general.search_format_default.name)
        dvd_order = self._parse_boolean(data.get('dvdOrder', 'false'))
        skip_downloaded = self._parse_boolean(data.get('skipDownloaded', sickrage.app.config.general.skip_downloaded_default))
        add_show_year = self._parse_boolean(data.get('addShowYear', 'false'))
152

153
        if not series_id:
154
            return self._bad_request(error=f"Missing seriesId parameter: {series_id}")
155

156
        series_provider_id = SeriesProviderID(series_provider_slug)
157
        if not series_provider_id:
158
            return self._not_found(error="Unable to identify a series provider using provided slug")
159
160

        series = find_show(int(series_id), series_provider_id)
161
        if series:
162
            return self._bad_request(error=f"Already exists series: {series_id}")
163

164
        if is_existing and not series_directory:
165
            return self._bad_request(error="Missing seriesDirectory parameter")
166

167
168
        if not is_existing:
            series_directory = os.path.join(root_directory, sanitize_file_name(series_name))
169

170
171
172
173
            if first_aired:
                series_year = re.search(r'\d{4}', first_aired)
                if add_show_year and not re.match(r'.*\(\d+\)$', series_directory) and series_year:
                    series_directory = f"{series_directory} ({series_year.group()})"
174

175
176
            if os.path.isdir(series_directory):
                sickrage.app.alerts.error(_("Unable to add show"), _("Folder ") + series_directory + _(" exists already"))
177
                return self._bad_request(error=f"Show directory {series_directory} already exists!")
178
179
180
181

            if not make_dir(series_directory):
                sickrage.app.log.warning(f"Unable to create the folder {series_directory}, can't add the show")
                sickrage.app.alerts.error(_("Unable to add show"), _(f"Unable to create the folder {series_directory}, can't add the show"))
182
                return self._bad_request(error=f"Unable to create the show folder {series_directory}, can't add the show")
183
184

        chmod_as_parent(series_directory)
185

186
        try:
echel0n's avatar
echel0n committed
187
            new_quality = Qualities[quality_preset.upper()]
188
        except (AttributeError, KeyError):
echel0n's avatar
echel0n committed
189
            new_quality = Quality.combine_qualities([Qualities[x.upper()] for x in allowed_qualities], [Qualities[x.upper()] for x in preferred_qualities])
190

191
192
        sickrage.app.show_queue.add_show(series_provider_id=series_provider_id,
                                         series_id=int(series_id),
193
                                         showDir=series_directory,
echel0n's avatar
echel0n committed
194
195
                                         default_status=EpisodeStatus[default_status.upper()],
                                         default_status_after=EpisodeStatus[default_status_after.upper()],
196
                                         quality=new_quality,
echel0n's avatar
echel0n committed
197
198
199
200
201
202
203
                                         flatten_folders=flatten_folders,
                                         lang=series_provider_language,
                                         subtitles=subtitles,
                                         sub_use_sr_metadata=sub_use_sr_metadata,
                                         anime=is_anime,
                                         dvd_order=dvd_order,
                                         search_format=SearchFormat[search_format.upper()],
204
205
206
                                         paused=False,
                                         # blacklist=blacklist,
                                         # whitelist=whitelist,
echel0n's avatar
echel0n committed
207
208
                                         scene=is_scene,
                                         skip_downloaded=skip_downloaded)
209
210
211

        sickrage.app.alerts.message(_('Adding Show'), _(f'Adding the specified show into {series_directory}'))

212
        return self.json_response({'message': True})
213

214
    def patch(self, series_slug):
215
216
217
218
219
220
221
        warnings, errors = [], []

        do_update = False
        do_update_exceptions = False

        data = json_decode(self.request.body)

222
223
        series = find_show_by_slug(series_slug)
        if series is None:
224
            return self._bad_request(error=f"Unable to find the specified series using slug: {series_slug}")
225
226
227
228
229
230
231
232
233
234
235
236
237
238

        # if we changed the language then kick off an update
        if data.get('lang') is not None and data['lang'] != series.lang:
            do_update = True

        if data.get('paused') is not None:
            series.paused = checkbox_to_value(data['paused'])

        if data.get('anime') is not None:
            series.anime = checkbox_to_value(data['anime'])

        if data.get('scene') is not None:
            series.scene = checkbox_to_value(data['scene'])

239
240
        if data.get('searchFormat') is not None:
            series.search_format = SearchFormat[data['searchFormat']]
241
242
243
244

        if data.get('subtitles') is not None:
            series.subtitles = checkbox_to_value(data['subtitles'])

245
246
        if data.get('subUseSrMetadata') is not None:
            series.sub_use_sr_metadata = checkbox_to_value(data['subUseSrMetadata'])
247

248
249
        if data.get('defaultEpStatus') is not None:
            series.default_ep_status = int(data['defaultEpStatus'])
250

251
252
        if data.get('skipDownloaded') is not None:
            series.skip_downloaded = checkbox_to_value(data['skipDownloaded'])
253

254
        if data.get('sceneExceptions') is not None and set(data['sceneExceptions']) != set(series.scene_exceptions):
255
256
257
258
259
260
261
262
263
264
            do_update_exceptions = True

        if data.get('whitelist') is not None:
            shortwhitelist = short_group_names(data['whitelist'])
            series.release_groups.set_white_keywords(shortwhitelist)

        if data.get('blacklist') is not None:
            shortblacklist = short_group_names(data['blacklist'])
            series.release_groups.set_black_keywords(shortblacklist)

265
266
267
268
269
270
        if data.get('qualityPreset') is not None:
            try:
                new_quality = Qualities[data['qualityPreset']]
            except KeyError:
                new_quality = Quality.combine_qualities([Qualities[x] for x in data['allowedQualities']], [Qualities[x] for x in data['preferredQualities']])

271
272
            series.quality = new_quality

273
274
        if data.get('flattenFolders') is not None and bool(series.flatten_folders) != bool(data['flattenFolders']):
            series.flatten_folders = data['flattenFolders']
275
            try:
276
                sickrage.app.show_queue.refresh_show(series.series_id, series.series_provider_id, True)
277
            except CantRefreshShowException as e:
278
                errors.append(_(f"Unable to refresh this show: {e}"))
279
280
281
282

        if data.get('language') is not None:
            series.lang = data['language']

283
284
        if data.get('dvdOrder') is not None:
            series.dvd_order = checkbox_to_value(data['dvdOrder'])
285

286
287
        if data.get('rlsIgnoreWords') is not None:
            series.rls_ignore_words = data['rlsIgnoreWords']
288

289
290
        if data.get('rlsRequireWords') is not None:
            series.rls_require_words = data['rlsRequireWords']
291
292
293
294
295
296

        # series.search_delay = int(data['search_delay'])

        # if we change location clear the db of episodes, change it, write to db, and rescan
        if data.get('location') is not None and os.path.normpath(series.location) != os.path.normpath(data['location']):
            sickrage.app.log.debug(os.path.normpath(series.location) + " != " + os.path.normpath(data['location']))
297
298
            if not os.path.isdir(data['location']) and not sickrage.app.config.general.create_missing_show_dirs:
                warnings.append(f"New location {data['location']} does not exist")
299
300
301
302
303
304
305

            # don't bother if we're going to update anyway
            elif not do_update:
                # change it
                try:
                    series.location = data['location']
                    try:
306
                        sickrage.app.show_queue.refresh_show(series.series_id, series.series_provider_id, True)
307
                    except CantRefreshShowException as e:
308
                        errors.append(_(f"Unable to refresh this show: {e}"))
309
                        # grab updated info from TVDB
310
                        # showObj.loadEpisodesFromSeriesProvider()
311
312
                        # rescan the episodes in the new folder
                except NoNFOException:
313
314
                    warnings.append(_(
                        f"The folder at {data['location']} doesn't contain a tvshow.nfo - copy your files to that folder before you change the directory in SiCKRAGE."))
315
316
317
318

        # force the update
        if do_update:
            try:
319
                sickrage.app.show_queue.update_show(series.series_id, series.series_provider_id, force=True)
320
            except CantUpdateShowException as e:
321
                errors.append(_(f"Unable to update show: {e}"))
322
323
324

        if do_update_exceptions:
            try:
325
                series.scene_exceptions = set(data['sceneExceptions'].split(','))
326
327
328
329
330
            except CantUpdateShowException:
                warnings.append(_("Unable to force an update on scene exceptions of the show."))

        # if do_update_scene_numbering:
        #     try:
331
        #         xem_refresh(series.series_id, series.series_provider_id, True)
332
333
334
335
336
337
        #     except CantUpdateShowException:
        #         warnings.append(_("Unable to force an update on scene numbering of the show."))

        # commit changes to database
        series.save()

338
        return self.json_response(series.to_json(episodes=True, details=True))
339

340
    def delete(self, series_slug):
341
342
        data = json_decode(self.request.body)

343
344
        series = find_show_by_slug(series_slug)
        if series is None:
345
            return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
346

347
        sickrage.app.show_queue.remove_show(series.series_id, series.series_provider_id, checkbox_to_value(data.get('delete')))
348

349
        return self.json_response({'message': True})
350
351


352
class ApiV2SeriesEpisodesHandler(ApiV2BaseHandler):
353
354
355
    def get(self, series_slug, *args, **kwargs):
        series = find_show_by_slug(series_slug)
        if series is None:
356
            return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
357
358
359
360
361

        episodes = []
        for episode in series.episodes:
            episodes.append(episode.to_json())

362
        return self.json_response(episodes)
363
364


365
class ApiV2SeriesImagesHandler(ApiV2BaseHandler):
366
367
368
    def get(self, series_slug, *args, **kwargs):
        series = find_show_by_slug(series_slug)
        if series is None:
369
            return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
echel0n's avatar
echel0n committed
370

371
        image = series_image(series.series_id, series.series_provider_id, SeriesImageType.POSTER_THUMB)
372
        return self.json_response({'poster': image.url})
373
374


375
class ApiV2SeriesImdbInfoHandler(ApiV2BaseHandler):
376
377
378
    def get(self, series_slug, *args, **kwargs):
        series = find_show_by_slug(series_slug)
        if series is None:
379
            return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
echel0n's avatar
echel0n committed
380

381
        with sickrage.app.main_db.session() as session:
382
            imdb_info = session.query(MainDB.IMDbInfo).filter_by(imdb_id=series.imdb_id).one_or_none()
383
384
            json_data = IMDbInfoSchema().dump(imdb_info)

385
        return self.json_response(json_data)
386
387


388
class ApiV2SeriesBlacklistHandler(ApiV2BaseHandler):
389
390
391
    def get(self, series_slug, *args, **kwargs):
        series = find_show_by_slug(series_slug)
        if series is None:
392
            return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
echel0n's avatar
echel0n committed
393

394
        with sickrage.app.main_db.session() as session:
395
            blacklist = session.query(MainDB.Blacklist).filter_by(series_id=series.series_id, series_provider_id=series.series_provider_id).one_or_none()
396
397
            json_data = BlacklistSchema().dump(blacklist)

398
        return self.json_response(json_data)
399
400


401
class ApiV2SeriesWhitelistHandler(ApiV2BaseHandler):
402
403
404
    def get(self, series_slug, *args, **kwargs):
        series = find_show_by_slug(series_slug)
        if series is None:
405
            return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
echel0n's avatar
echel0n committed
406

407
        with sickrage.app.main_db.session() as session:
408
            whitelist = session.query(MainDB.Whitelist).filter_by(series_id=series.series_id, series_provider_id=series.series_provider_id).one_or_none()
409
410
            json_data = WhitelistSchema().dump(whitelist)

411
        return self.json_response(json_data)
412
413


414
class ApiV2SeriesRefreshHandler(ApiV2BaseHandler):
415
    def get(self, series_slug):
416
417
        force = self.get_argument('force', None)

418
        series = find_show_by_slug(series_slug)
419
        if series is None:
420
            return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
421
422

        try:
423
            sickrage.app.show_queue.refresh_show(series.series_id, series.series_provider_id, force=bool(force))
424
        except CantUpdateShowException as e:
425
            return self._bad_request(error=_(f"Unable to refresh this show, error: {e}"))
426
427


428
class ApiV2SeriesUpdateHandler(ApiV2BaseHandler):
429
    def get(self, series_slug):
430
431
        force = self.get_argument('force', None)

432
        series = find_show_by_slug(series_slug)
433
        if series is None:
434
            return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
435
436

        try:
437
            sickrage.app.show_queue.update_show(series.series_id, series.series_provider_id, force=bool(force))
438
        except CantUpdateShowException as e:
439
            return self._bad_request(error=_(f"Unable to update this show, error: {e}"))
440
441


442
class ApiV2SeriesEpisodesRenameHandler(ApiV2BaseHandler):
echel0n's avatar
echel0n committed
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
    def get(self, series_slug):
        """Get list of episodes to rename"
        ---
        tags: [Series]
        summary: Get list of episodes to rename
        description: Get list of episodes to rename
        parameters:
        - in: path
          schema:
            SeriesSlugPath
        responses:
          200:
            description: Success payload
            content:
              application/json:
                schema:
                  SeriesEpisodesRenameSuccessSchema
          400:
            description: Bad request; Check `errors` for any validation errors
            content:
              application/json:
                schema:
                  BadRequestSchema
          401:
            description: Returned if your JWT token is missing or expired
            content:
              application/json:
                schema:
                  NotAuthorizedSchema
        """
        if not series_slug:
474
            return self._bad_request(error="Missing series slug")
echel0n's avatar
echel0n committed
475
476
477
478
479

        rename_data = []

        series = find_show_by_slug(series_slug)
        if series is None:
480
            return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
echel0n's avatar
echel0n committed
481
482

        if not os.path.isdir(series.location):
483
            return self._bad_request(error="Can't rename episodes when the show location does not exist")
echel0n's avatar
echel0n committed
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500

        for episode in series.episodes:
            if not episode.location:
                continue

            current_location = episode.location[len(episode.show.location) + 1:]
            new_location = "{}.{}".format(episode.proper_path(), current_location.split('.')[-1])

            if current_location != new_location:
                rename_data.append({
                    'episodeId': episode.episode_id,
                    'season': episode.season,
                    'episode': episode.episode,
                    'currentLocation': current_location,
                    'newLocation': new_location,
                })

501
        return self.json_response(rename_data)
echel0n's avatar
echel0n committed
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538

    def post(self, series_slug):
        """Rename list of episodes"
        ---
        tags: [Series]
        summary: Rename list of episodes
        description: Rename list of episodes
        parameters:
        - in: path
          schema:
            SeriesSlugPath
        responses:
          200:
            description: Success payload
            content:
              application/json:
                schema:
                  EpisodesRenameSuccessSchema
          400:
            description: Bad request; Check `errors` for any validation errors
            content:
              application/json:
                schema:
                  BadRequestSchema
          401:
            description: Returned if your JWT token is missing or expired
            content:
              application/json:
                schema:
                  NotAuthorizedSchema
        """
        data = json_decode(self.request.body)

        renamed_episodes = []

        series = find_show_by_slug(series_slug)
        if series is None:
539
            return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
echel0n's avatar
echel0n committed
540
541

        if not os.path.isdir(series.location):
542
            return self._bad_request(error="Can't rename episodes when the show location does not exist")
echel0n's avatar
echel0n committed
543
544
545
546
547
548
549
550
551
552

        for episode_id in data.get('episodeIdList', []):
            episode = find_episode(episode_id, series.series_provider_id)
            if episode:
                episode.rename()
                renamed_episodes.append(episode.episode_id)

        if len(renamed_episodes) > 0:
            WebSocketMessage('SHOW_RENAMED', {'seriesSlug': series.slug}).push()

553
        return self.json_response(renamed_episodes)
echel0n's avatar
echel0n committed
554
555


556
class ApiV2SeriesEpisodesManualSearchHandler(ApiV2BaseHandler):
echel0n's avatar
echel0n committed
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
    def get(self, series_slug, episode_slug):
        """Episode Manual Search"
        ---
        tags: [Series]
        summary: Manually search for episode on search providers
        description: Manually search for episode on search providers
        parameters:
        - in: path
          schema:
            SeriesSlugPath
        - in: path
          schema:
            EpisodeSlugPath
        responses:
          200:
            description: Success payload
            content:
              application/json:
                schema:
                  EpisodesManualSearchSuccessSchema
          400:
            description: Bad request; Check `errors` for any validation errors
            content:
              application/json:
                schema:
                  BadRequestSchema
          401:
            description: Returned if your JWT token is missing or expired
            content:
              application/json:
                schema:
                  NotAuthorizedSchema
          404:
            description: Returned if the given episode slug does not exist or the search returns no results.
            content:
              application/json:
                schema:
                  NotFoundSchema
        """
        use_existing_quality = self.get_argument('useExistingQuality', None) or False

        # validation_errors = self._validate_schema(SeriesEpisodesManualSearchPath, self.request.path)
        # if validation_errors:
600
        #     return self._bad_request(error=validation_errors)
echel0n's avatar
echel0n committed
601
602
603
        #
        # validation_errors = self._validate_schema(SeriesEpisodesManualSearchSchema, self.request.arguments)
        # if validation_errors:
604
        #     return self._bad_request(error=validation_errors)
echel0n's avatar
echel0n committed
605
606
607
608
        #

        series = find_show_by_slug(series_slug)
        if series is None:
609
            return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
echel0n's avatar
echel0n committed
610
611
612
613
614
615
616

        match = re.match(r'^s(?P<season>\d+)e(?P<episode>\d+)$', episode_slug)
        season_num = match.group('season')
        episode_num = match.group('episode')

        episode = series.get_episode(int(season_num), int(episode_num), no_create=True)
        if episode is None:
617
            return self._bad_request(error=f"Unable to find the specified episode using slug: {episode_slug}")
echel0n's avatar
echel0n committed
618
619
620
621
622
623
624
625
626
627

        # make a queue item for it and put it on the queue
        ep_queue_item = ManualSearchTask(int(episode.show.series_id),
                                         episode.show.series_provider_id,
                                         int(episode.season),
                                         int(episode.episode),
                                         bool(use_existing_quality))

        sickrage.app.search_queue.put(ep_queue_item)
        if not all([ep_queue_item.started, ep_queue_item.success]):
628
            return self.json_response({'success': True})
echel0n's avatar
echel0n committed
629

630
        return self._not_found(error=_(f"Unable to find season {season_num} episode {episode_num} for show {series.name} on search providers"))
631
632
633
634
635
636
637
638
639
640


class ApiV2SeriesSearchFormatsHandler(ApiV2BaseHandler):
    def get(self):
        search_formats = [{
            'name': x.display_name,
            'slug': x.name,
        } for x in SearchFormat]

        return self.json_response(search_formats)