__init__.py 26.8 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

            if not make_dir(series_directory):
                sickrage.app.log.warning(f"Unable to create the folder {series_directory}, can't add the show")
echel0n's avatar
echel0n committed
181
                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

echel0n's avatar
echel0n committed
210
        sickrage.app.alerts.message(_('Adding Show'), f'Adding the specified show into {series_directory}')
211

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:
echel0n's avatar
echel0n committed
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:
echel0n's avatar
echel0n committed
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:
echel0n's avatar
echel0n committed
313
                    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.")
314
315
316
317

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

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

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

        # commit changes to database
        series.save()

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

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

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

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

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


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

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

361
        return self.json_response(episodes)
362
363


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

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


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

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

384
        return self.json_response(json_data)
385
386


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

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

397
        return self.json_response(json_data)
398
399


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

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

410
        return self.json_response(json_data)
411
412


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

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

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


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

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

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


441
class ApiV2SeriesEpisodesRenameHandler(ApiV2BaseHandler):
echel0n's avatar
echel0n committed
442
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
    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:
473
            return self._bad_request(error="Missing series slug")
echel0n's avatar
echel0n committed
474
475
476
477
478

        rename_data = []

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

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

        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,
                })

500
        return self.json_response(rename_data)
echel0n's avatar
echel0n committed
501
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

    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:
538
            return self._not_found(error=f"Unable to find the specified series using slug: {series_slug}")
echel0n's avatar
echel0n committed
539
540

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

        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()

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


555
class ApiV2SeriesEpisodesManualSearchHandler(ApiV2BaseHandler):
echel0n's avatar
echel0n committed
556
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
    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:
599
        #     return self._bad_request(error=validation_errors)
echel0n's avatar
echel0n committed
600
601
602
        #
        # validation_errors = self._validate_schema(SeriesEpisodesManualSearchSchema, self.request.arguments)
        # if validation_errors:
603
        #     return self._bad_request(error=validation_errors)
echel0n's avatar
echel0n committed
604
605
606
607
        #

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

        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:
616
            return self._bad_request(error=f"Unable to find the specified episode using slug: {episode_slug}")
echel0n's avatar
echel0n committed
617
618
619
620
621
622
623
624
625
626

        # 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]):
627
            return self.json_response({'success': True})
echel0n's avatar
echel0n committed
628

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


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)