views.py 212 KB
Newer Older
1
# Author: echel0n <[email protected]>
echel0n's avatar
echel0n committed
2
# URL: https://sickrage.ca
3
#
echel0n's avatar
echel0n committed
4
# This file is part of SickRage.
5
#
echel0n's avatar
echel0n committed
6
# SickRage is free software: you can redistribute it and/or modify
7
8
9
10
# 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.
#
echel0n's avatar
echel0n committed
11
# SickRage is distributed in the hope that it will be useful,
12
13
14
15
16
# 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
echel0n's avatar
echel0n committed
17
# along with SickRage.  If not, see <http://www.gnu.org/licenses/>.
18

19
20
from __future__ import unicode_literals

21
import datetime
22
import io
23
import os
24
import re
echel0n's avatar
echel0n committed
25
import threading
26
import time
27
import traceback
28
import urllib
29
from collections import OrderedDict
30
from urlparse import urlparse, urljoin
31

echel0n's avatar
echel0n committed
32
import dateutil.tz
33
import markdown2
echel0n's avatar
echel0n committed
34
import tornado.locale
echel0n's avatar
echel0n committed
35
from CodernityDB.database import RecordNotFound
echel0n's avatar
echel0n committed
36
from concurrent.futures import ThreadPoolExecutor
37
from mako.exceptions import RichTraceback
38
from mako.lookup import TemplateLookup
39
from requests import HTTPError
echel0n's avatar
echel0n committed
40
from tornado.concurrent import run_on_executor
41
from tornado.escape import json_encode, recursive_unicode
echel0n's avatar
echel0n committed
42
43
from tornado.gen import coroutine
from tornado.process import cpu_count
44
from tornado.web import RequestHandler, authenticated
45
46

import sickrage
echel0n's avatar
echel0n committed
47
import sickrage.subtitles
echel0n's avatar
echel0n committed
48
from adba import aniDBAbstracter
49
from sickrage.clients import getClientIstance
50
from sickrage.clients.sabnzbd import SabNZBd
51
from sickrage.core import API, google_drive
52
from sickrage.core.blackandwhitelist import BlackAndWhiteList, \
53
    short_group_names
54
from sickrage.core.classes import ErrorViewer, AllShowsUI
55
56
from sickrage.core.classes import WarningViewer
from sickrage.core.common import FAILED, IGNORED, Overview, Quality, SKIPPED, \
57
    SNATCHED, UNAIRED, WANTED, cpu_presets, statusStrings
58
from sickrage.core.exceptions import CantRefreshShowException, \
59
    CantUpdateShowException, EpisodeDeletedException, \
echel0n's avatar
echel0n committed
60
    NoNFOException, CantRemoveShowException
echel0n's avatar
echel0n committed
61
62
from sickrage.core.helpers import argToBool, backupSR, chmodAsParent, findCertainShow, generateApiKey, \
    getDiskSpaceUsage, makeDir, readFileBuffered, \
63
    remove_article, restoreConfigZip, \
64
    sanitizeFileName, clean_url, try_int, torrent_webui_url, checkbox_to_value, clean_host, \
65
    clean_hosts, app_statistics
66
from sickrage.core.helpers.browser import foldersAtPath
67
from sickrage.core.helpers.compat import cmp
68
from sickrage.core.helpers.srdatetime import srDateTime
69
from sickrage.core.imdb_popular import imdbPopular
70
71
from sickrage.core.nameparser import validator
from sickrage.core.queues.search import BacklogQueueItem, FailedQueueItem, \
72
    MANUAL_SEARCH_HISTORY, ManualSearchQueueItem
echel0n's avatar
echel0n committed
73
from sickrage.core.scene_exceptions import get_scene_exceptions, update_scene_exceptions
74
from sickrage.core.scene_numbering import get_scene_absolute_numbering, \
75
76
    get_scene_absolute_numbering_for_show, get_scene_numbering, \
    get_scene_numbering_for_show, get_xem_absolute_numbering_for_show, \
77
    get_xem_numbering_for_show, set_scene_numbering
echel0n's avatar
echel0n committed
78
from sickrage.core.traktapi import srTraktAPI
79
80
81
from sickrage.core.tv.episode import TVEpisode
from sickrage.core.tv.show.coming_episodes import ComingEpisodes
from sickrage.core.tv.show.history import History as HistoryTool
echel0n's avatar
echel0n committed
82
from sickrage.core.webserver import ApiHandler
83
from sickrage.core.webserver.routes import Route
84
from sickrage.indexers import IndexerApi
85
from sickrage.providers import NewznabProvider, TorrentRssProvider
86

echel0n's avatar
echel0n committed
87

88
class BaseHandler(RequestHandler):
89
90
    def __init__(self, application, request, **kwargs):
        super(BaseHandler, self).__init__(application, request, **kwargs)
echel0n's avatar
echel0n committed
91
        self.executor = ThreadPoolExecutor(cpu_count())
echel0n's avatar
echel0n committed
92
        self.startTime = time.time()
93

94
        # template settings
95
        self.mako_lookup = TemplateLookup(
96
            directories=[sickrage.app.config.gui_views_dir],
97
            module_directory=os.path.join(sickrage.app.cache_dir, 'mako'),
echel0n's avatar
echel0n committed
98
            filesystem_checks=True,
echel0n's avatar
echel0n committed
99
            strict_undefined=True,
echel0n's avatar
echel0n committed
100
101
102
103
            input_encoding='utf-8',
            output_encoding='utf-8',
            encoding_errors='replace',
            future_imports=['unicode_literals']
104
        )
105

echel0n's avatar
echel0n committed
106
    def get_user_locale(self):
107
        return tornado.locale.get(sickrage.app.config.gui_lang)
echel0n's avatar
echel0n committed
108

109
110
111
    def write_error(self, status_code, **kwargs):
        # handle 404 http errors
        if status_code == 404:
112
            url = self.request.uri
113
114
            if sickrage.app.config.web_root and self.request.uri.startswith(sickrage.app.config.web_root):
                url = url[len(sickrage.app.config.web_root) + 1:]
115

116
            if url[:3] != 'api':
117
                self.write(self.render(
118
                    '/errors/404.mako',
echel0n's avatar
echel0n committed
119
                    title=_('HTTP Error 404'),
120
                    header=_('HTTP Error 404')))
121
            else:
122
                self.write('Wrong API key used')
123
        elif self.settings.get("debug") and "exc_info" in kwargs:
124
            exc_info = kwargs["exc_info"]
125
126
            trace_info = ''.join(["%s<br>" % line for line in traceback.format_exception(*exc_info)])
            request_info = ''.join(["<strong>%s</strong>: %s<br>" % (k, self.request.__dict__[k]) for k in
127
128
129
130
                                    self.request.__dict__.keys()])
            error = exc_info[1]

            self.set_header('Content-Type', 'text/html')
131
            self.write("""<html>
132
133
134
135
                             <title>{error}</title>
                             <body>
                                <button onclick="window.location='{webroot}/logs/';">View Log(Errors)</button>
                                <button onclick="window.location='{webroot}/home/restart?force=1';">Restart SiCKRAGE</button>
echel0n's avatar
echel0n committed
136
                                <button onclick="window.location='{webroot}/logout';">Logout</button>
137
138
139
140
141
142
143
144
145
146
147
                                <h2>Error</h2>
                                <p>{error}</p>
                                <h2>Traceback</h2>
                                <p>{traceback}</p>
                                <h2>Request Info</h2>
                                <p>{request}</p>
                             </body>
                           </html>""".format(error=error,
                                             traceback=trace_info,
                                             request=request_info,
                                             webroot=sickrage.app.config.web_root))
148

Dustyn Gibson's avatar
Dustyn Gibson committed
149
    def get_current_user(self):
150
151
        try:
            try:
echel0n's avatar
echel0n committed
152
                return sickrage.app.oidc_client.userinfo(self.get_secure_cookie('sr_access_token'))
153
154
155
156
157
158
159
            except HTTPError:
                token = sickrage.app.oidc_client.refresh_token(self.get_secure_cookie('sr_refresh_token'))
                self.set_secure_cookie('sr_access_token', token['access_token'])
                self.set_secure_cookie('sr_refresh_token', token['refresh_token'])
                return sickrage.app.oidc_client.userinfo(token['access_token'])
        except Exception:
            pass
160

161
    def render_string(self, template_name, **kwargs):
162
        template_kwargs = {
163
164
165
166
            'title': "",
            'header': "",
            'topmenu': "",
            'submenu': "",
167
168
            'controller': "home",
            'action': "index",
169
            'srPID': sickrage.app.pid,
170
            'srHttpsEnabled': sickrage.app.config.enable_https or bool(
echel0n's avatar
echel0n committed
171
                self.request.headers.get('X-Forwarded-Proto') == 'https'),
172
            'srHost': self.request.headers.get('X-Forwarded-Host', self.request.host.split(':')[0]),
173
174
175
176
177
178
            'srHttpPort': self.request.headers.get('X-Forwarded-Port', sickrage.app.config.web_port),
            'srHttpsPort': sickrage.app.config.web_port,
            'srHandleReverseProxy': sickrage.app.config.handle_reverse_proxy,
            'srThemeName': sickrage.app.config.theme_name,
            'srDefaultPage': sickrage.app.config.default_page,
            'srWebRoot': sickrage.app.config.web_root,
179
180
            'srLocale': self.get_user_locale().code,
            'srLocaleDir': sickrage.LOCALE_DIR,
181
182
183
            'numErrors': len(ErrorViewer.errors),
            'numWarnings': len(WarningViewer.errors),
            'srStartTime': self.startTime,
184
            'makoStartTime': time.time(),
echel0n's avatar
echel0n committed
185
            'overall_stats': None,
echel0n's avatar
echel0n committed
186
            'torrent_webui_url': torrent_webui_url(),
187
            'application': self.application,
echel0n's avatar
echel0n committed
188
            'request': self.request,
189
190
        }

191
        template_kwargs.update(self.get_template_namespace())
192
        template_kwargs.update(kwargs)
193

194
195
        try:
            return self.mako_lookup.get_template(template_name).render_unicode(**template_kwargs)
196
        except Exception:
echel0n's avatar
echel0n committed
197
198
            kwargs['title'] = _('HTTP Error 500')
            kwargs['header'] = _('HTTP Error 500')
199
200
201
            kwargs['backtrace'] = RichTraceback()
            template_kwargs.update(kwargs)
            return self.mako_lookup.get_template('/errors/500.mako').render_unicode(**template_kwargs)
202

203
204
    def render(self, template_name, **kwargs):
        return self.render_string(template_name, **kwargs)
205

echel0n's avatar
echel0n committed
206
    @run_on_executor
207
    def worker(self, function, **kwargs):
echel0n's avatar
echel0n committed
208
        threading.currentThread().setName("TORNADO")
209
        kwargs = recursive_unicode(kwargs)
210
211
212
        for arg, value in kwargs.items():
            if len(value) == 1:
                kwargs[arg] = value[0]
echel0n's avatar
echel0n committed
213

214
        return function(**kwargs)
215

echel0n's avatar
echel0n committed
216
217
218
219
220
    def set_default_headers(self):
        self.set_header("Access-Control-Allow-Origin", "*")
        self.set_header("Access-Control-Allow-Headers", "x-requested-with")
        self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS')
        self.set_header('Cache-Control', 'max-age=0,no-cache,no-store')
221

222
    def redirect(self, url, permanent=True, status=None):
223
224
        if sickrage.app.config.web_root not in url:
            url = urljoin(sickrage.app.config.web_root + '/', url.lstrip('/'))
225
226
227
228
229
230
        super(BaseHandler, self).redirect(url, permanent, status)

    def previous_url(self):
        url = urlparse(self.request.headers.get("referer", "/{}/".format(sickrage.app.config.default_page)))
        return url._replace(scheme="", netloc="").geturl()

231

232
class WebHandler(BaseHandler):
233
234
    def __init__(self, *args, **kwargs):
        super(WebHandler, self).__init__(*args, **kwargs)
235

echel0n's avatar
echel0n committed
236
    @coroutine
237
    @authenticated
238
239
240
241
242
243
244
245
246
247
248
    def get(self, *args, **kwargs):
        result = yield self.route()
        if result: self.write(result)

    @coroutine
    @authenticated
    def post(self, *args, **kwargs):
        result = yield self.route()
        if result: self.write(result)

    def route(self):
249
        # route -> method obj
250
251
        method = getattr(
            self, self.request.path.strip('/').split('/')[::-1][0].replace('.', '_'),
252
            getattr(self, 'index', None)
253
        )
echel0n's avatar
echel0n committed
254

255
        if method: return self.worker(method, **self.request.arguments)
256

257
258
259
260
261
262
263
264
265
    def _genericMessage(self, subject, message):
        return self.render(
            "/generic_message.mako",
            message=message,
            subject=subject,
            title="",
            controller='root',
            action='genericmessage'
        )
266

267

echel0n's avatar
echel0n committed
268
class LoginHandler(BaseHandler):
269
270
271
    def __init__(self, *args, **kwargs):
        super(LoginHandler, self).__init__(*args, **kwargs)

echel0n's avatar
echel0n committed
272
273
    def prepare(self, *args, **kwargs):
        redirect_uri = "{}://{}{}/login".format(self.request.protocol, self.request.host, sickrage.app.config.web_root)
274

echel0n's avatar
echel0n committed
275
276
        code = self.get_argument('code', False)
        if code:
277
            try:
278
                token = sickrage.app.oidc_client.authorization_code(code, redirect_uri)
279
280
                userinfo = sickrage.app.oidc_client.userinfo(token['access_token'])

281
282
283
                self.set_secure_cookie('sr_access_token', token['access_token'])
                self.set_secure_cookie('sr_refresh_token', token['refresh_token'])

284
285
286
287
288
289
290
291
                if not userinfo.get('sub'):
                    return self.redirect('/logout')

                if not sickrage.app.config.app_sub:
                    sickrage.app.config.app_sub = userinfo.get('sub')
                    sickrage.app.config.save()
                elif sickrage.app.config.app_sub != userinfo.get('sub'):
                    if API().token:
292
                        allowed_usernames = API().allowed_usernames()['data']
293
                        if not userinfo['preferred_username'] in allowed_usernames:
294
295
296
297
                            sickrage.app.log.debug(
                                "USERNAME:{} IP:{} - ACCESS DENIED".format(userinfo['preferred_username'],
                                                                           self.request.remote_ip)
                            )
echel0n's avatar
echel0n committed
298
                            return self.redirect('/logout')
299
300
301
302
303
304
                    else:
                        return self.redirect('/logout')

                if not API().token:
                    exchange = {'scope': 'offline_access', 'subject_token': token['access_token']}
                    API().token = sickrage.app.oidc_client.token_exchange(**exchange)
echel0n's avatar
echel0n committed
305
            except Exception as e:
306
307
                return self.redirect('/logout')

308
309
            redirect_uri = self.get_argument('next', "/{}/".format(sickrage.app.config.default_page))
            return self.redirect("{}".format(redirect_uri))
echel0n's avatar
echel0n committed
310
        else:
311
312
            authorization_url = sickrage.app.oidc_client.authorization_url(redirect_uri=redirect_uri)
            return super(BaseHandler, self).redirect(authorization_url)
echel0n's avatar
echel0n committed
313

314

echel0n's avatar
echel0n committed
315
class LogoutHandler(BaseHandler):
316
    def __init__(self, *args, **kwargs):
317
        super(LogoutHandler, self).__init__(*args, **kwargs)
318

319
    def prepare(self, *args, **kwargs):
320
321
322
        logout_uri = sickrage.app.oidc_client.get_url('end_session_endpoint')
        redirect_uri = "{}://{}{}/login".format(self.request.protocol, self.request.host, sickrage.app.config.web_root)

echel0n's avatar
echel0n committed
323
324
        if self.get_secure_cookie('sr_refresh_token'):
            sickrage.app.oidc_client.logout(self.get_secure_cookie('sr_refresh_token'))
echel0n's avatar
echel0n committed
325

echel0n's avatar
echel0n committed
326
        self.clear_all_cookies()
327
328

        return super(BaseHandler, self).redirect('{}?redirect_uri={}'.format(logout_uri, redirect_uri))
329

330

echel0n's avatar
echel0n committed
331
332
class CalendarHandler(BaseHandler):
    def prepare(self, *args, **kwargs):
333
        if sickrage.app.config.calendar_unprotected:
echel0n's avatar
echel0n committed
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
            self.write(self.calendar())
        else:
            self.calendar_auth()

    @authenticated
    def calendar_auth(self):
        self.write(self.calendar())

    # Raw iCalendar implementation by Pedro Jose Pereira Vieito (@pvieito).
    #
    # iCalendar (iCal) - Standard RFC 5545 <http://tools.ietf.org/html/rfc5546>
    # Works with iCloud, Google Calendar and Outlook.
    def calendar(self):
        """ Provides a subscribeable URL for iCal subscriptions
        """

echel0n's avatar
echel0n committed
350
        utc = dateutil.tz.gettz('GMT')
echel0n's avatar
echel0n committed
351

352
        sickrage.app.log.info("Receiving iCal request from %s" % self.request.remote_ip)
echel0n's avatar
echel0n committed
353
354
355
356
357
358

        # Create a iCal string
        ical = 'BEGIN:VCALENDAR\r\n'
        ical += 'VERSION:2.0\r\n'
        ical += 'X-WR-CALNAME:SiCKRAGE\r\n'
        ical += 'X-WR-CALDESC:SiCKRAGE\r\n'
echel0n's avatar
echel0n committed
359
        ical += 'PRODID://SiCKRAGE Upcoming Episodes//\r\n'
echel0n's avatar
echel0n committed
360
361
362
363
364
365

        # Limit dates
        past_date = (datetime.date.today() + datetime.timedelta(weeks=-52)).toordinal()
        future_date = (datetime.date.today() + datetime.timedelta(weeks=52)).toordinal()

        # Get all the shows that are not paused and are currently on air (from kjoconnor Fork)
366
367
        for show in [x for x in sickrage.app.showlist if
                     x.status.lower() in ['continuing', 'returning series'] and x.paused != 1]:
368
369
            for episode in (x for x in sickrage.app.main_db.get_many('tv_episodes', int(show.indexerid))
                            if past_date <= x['airdate'] < future_date):
echel0n's avatar
echel0n committed
370

echel0n's avatar
echel0n committed
371
372
                air_date_time = sickrage.app.tz_updater.parse_date_time(episode['airdate'], show.airs,
                                                                        show.network).astimezone(utc)
373
                air_date_time_end = air_date_time + datetime.timedelta(minutes=try_int(show.runtime, 60))
echel0n's avatar
echel0n committed
374
375
376

                # Create event for episode
                ical += 'BEGIN:VEVENT\r\n'
377
378
                ical += 'DTSTART:' + air_date_time.strftime("%Y%m%d") + 'T' + air_date_time.strftime("%H%M%S") + 'Z\r\n'
                ical += 'DTEND:' + air_date_time_end.strftime("%Y%m%d") + 'T' + air_date_time_end.strftime(
echel0n's avatar
echel0n committed
379
                    "%H%M%S") + 'Z\r\n'
380
                if sickrage.app.config.calendar_icons:
381
                    ical += 'X-GOOGLE-CALENDAR-CONTENT-ICON:https://www.sickrage.ca/favicon.ico\r\n'
echel0n's avatar
echel0n committed
382
383
                    ical += 'X-GOOGLE-CALENDAR-CONTENT-DISPLAY:CHIP\r\n'
                ical += 'SUMMARY: {0} - {1}x{2} - {3}\r\n'.format(
384
                    show.name, episode['season'], episode['episode'], episode['name']
echel0n's avatar
echel0n committed
385
386
                )
                ical += 'UID:SiCKRAGE-' + str(datetime.date.today().isoformat()) + '-' + \
387
                        show.name.replace(" ", "-") + '-E' + str(episode['episode']) + \
echel0n's avatar
echel0n committed
388
389
390
                        'S' + str(episode['season']) + '\r\n'
                if episode['description']:
                    ical += 'DESCRIPTION: {0} on {1} \\n\\n {2}\r\n'.format(
391
392
                        (show.airs or '(Unknown airs)'),
                        (show.network or 'Unknown network'),
echel0n's avatar
echel0n committed
393
394
                        episode['description'].splitlines()[0])
                else:
395
396
                    ical += 'DESCRIPTION:' + (show.airs or '(Unknown airs)') + ' on ' + (
                            show.network or 'Unknown network') + '\r\n'
echel0n's avatar
echel0n committed
397
398
399
400
401
402
403
404

                ical += 'END:VEVENT\r\n'

        # Ending the iCal
        ical += 'END:VCALENDAR'

        return ical

echel0n's avatar
echel0n committed
405

406
@Route('(.*)(/?.*)')
407
class WebRoot(WebHandler):
408
409
410
    def __init__(self, *args, **kwargs):
        super(WebRoot, self).__init__(*args, **kwargs)

411
    def index(self):
echel0n's avatar
echel0n committed
412
        return self.redirect("/{}/".format(sickrage.app.config.default_page))
413

414
    def robots_txt(self):
415
416
        """ Keep web crawlers out """
        self.set_header('Content-Type', 'text/plain')
echel0n's avatar
echel0n committed
417
        return "User-agent: *\nDisallow: /"
418

419
420
421
422
423
424
425
    def messages_po(self):
        """ Get /sickrage/locale/{lang_code}/LC_MESSAGES/messages.po """
        if sickrage.app.config.gui_lang:
            locale_file = os.path.join(sickrage.LOCALE_DIR, sickrage.app.config.gui_lang, 'LC_MESSAGES/messages.po')
            if os.path.isfile(locale_file):
                with io.open(locale_file, 'r', encoding='utf8') as f:
                    return f.read()
426

427
    def apibuilder(self):
428
        def titler(x):
429
            return (remove_article(x), x)[not x or sickrage.app.config.sort_article]
430

Gaëtan Muller's avatar
Gaëtan Muller committed
431
        episodes = {}
432

433
        for result in sorted((x for x in sickrage.app.main_db.all('tv_episodes')),
echel0n's avatar
echel0n committed
434
                             key=lambda d: (d['season'], d['episode'])):
435

436
437
            if result['showid'] not in episodes:
                episodes[result['showid']] = {}
438

439
440
            if result['season'] not in episodes[result['showid']]:
                episodes[result['showid']][result['season']] = []
441

442
            episodes[result['showid']][result['season']].append(result['episode'])
443

444
445
        if len(sickrage.app.config.api_key) == 32:
            apikey = sickrage.app.config.api_key
446
        else:
echel0n's avatar
echel0n committed
447
            apikey = _('API Key not generated')
448

449
450
        return self.render(
            'api_builder.mako',
echel0n's avatar
echel0n committed
451
452
            title=_('API Builder'),
            header=_('API Builder'),
453
            shows=sorted(sickrage.app.showlist, lambda x, y: cmp(titler(x.name), titler(y.name))),
454
455
            episodes=episodes,
            apikey=apikey,
echel0n's avatar
echel0n committed
456
            commands=ApiHandler(self.application, self.request).api_calls,
457
458
459
            controller='root',
            action='api_builder'
        )
460

461
    def setHomeLayout(self, layout):
Goeny's avatar
Goeny committed
462
        if layout not in ('poster', 'small', 'banner', 'simple', 'coverflow'):
463
464
            layout = 'poster'

465
        sickrage.app.config.home_layout = layout
466

labrys's avatar
labrys committed
467
        # Don't redirect to default page so user can see new layout
468
        return self.redirect("/home/")
469

Dustyn Gibson's avatar
Dustyn Gibson committed
470
471
    @staticmethod
    def setPosterSortBy(sort):
472
473
474
475

        if sort not in ('name', 'date', 'network', 'progress'):
            sort = 'name'

476
        sickrage.app.config.poster_sortby = sort
477
        sickrage.app.config.save()
478

Dustyn Gibson's avatar
Dustyn Gibson committed
479
480
    @staticmethod
    def setPosterSortDir(direction):
481

482
        sickrage.app.config.poster_sortdir = int(direction)
483
        sickrage.app.config.save()
484

485
486
487
488
489
    def setHistoryLayout(self, layout):

        if layout not in ('compact', 'detailed'):
            layout = 'detailed'

490
        sickrage.app.config.history_layout = layout
491

492
        return self.redirect("/history/")
493
494
495

    def toggleDisplayShowSpecials(self, show):

496
        sickrage.app.config.display_show_specials = not sickrage.app.config.display_show_specials
497

498
        return self.redirect("/home/displayShow?show=" + show)
499

500
    def setScheduleLayout(self, layout):
501
        if layout not in ('poster', 'banner', 'list', 'calendar'):
502
503
            layout = 'banner'

504
        if layout == 'calendar':
505
            sickrage.app.config.coming_eps_sort = 'date'
506

507
        sickrage.app.config.coming_eps_layout = layout
508

509
        return self.redirect("/schedule/")
510

511
    def toggleScheduleDisplayPaused(self):
512

513
        sickrage.app.config.coming_eps_display_paused = not sickrage.app.config.coming_eps_display_paused
514

515
        return self.redirect("/schedule/")
516

517
    def setScheduleSort(self, sort):
518
519
        if sort not in ('date', 'network', 'show'):
            sort = 'date'
520

521
        if sickrage.app.config.coming_eps_layout == 'calendar':
echel0n's avatar
echel0n committed
522
            sort = 'date'
523

524
        sickrage.app.config.coming_eps_sort = sort
525

526
        return self.redirect("/schedule/")
527

528
    def schedule(self, layout=None):
529
530
        next_week = datetime.date.today() + datetime.timedelta(days=7)
        next_week1 = datetime.datetime.combine(next_week,
531
                                               datetime.datetime.now().time().replace(tzinfo=sickrage.app.tz))
532
        results = ComingEpisodes.get_coming_episodes(ComingEpisodes.categories,
533
                                                     sickrage.app.config.coming_eps_sort,
echel0n's avatar
echel0n committed
534
                                                     False)
535
        today = datetime.datetime.now().replace(tzinfo=sickrage.app.tz)
536

537
        # Allow local overriding of layout parameter
538
        if layout and layout in ('poster', 'banner', 'list', 'calendar'):
Dustyn Gibson's avatar
Mako    
Dustyn Gibson committed
539
            layout = layout
540
        else:
541
            layout = sickrage.app.config.coming_eps_layout
542

543
544
545
546
547
548
        return self.render(
            'schedule.mako',
            next_week=next_week1,
            today=today,
            results=results,
            layout=layout,
echel0n's avatar
echel0n committed
549
550
            title=_('Schedule'),
            header=_('Schedule'),
551
552
553
554
            topmenu='schedule',
            controller='root',
            action='schedule'
        )
555

echel0n's avatar
echel0n committed
556
    def unlink(self):
557
        if not sickrage.app.config.app_sub == self.get_current_user().get('sub'):
558
559
            return self.redirect("/{}/".format(sickrage.app.config.default_page))

560
561
562
        sickrage.app.config.app_sub = ""
        sickrage.app.config.save()

echel0n's avatar
echel0n committed
563
        API().token = sickrage.app.oidc_client.logout(API().token['refresh_token'])
564

echel0n's avatar
echel0n committed
565
566
        return self.redirect('/logout/')

echel0n's avatar
echel0n committed
567
    def quicksearch_json(self, term):
568
569
        return json_encode(
            sickrage.app.quicksearch_cache.get_shows(term) + sickrage.app.quicksearch_cache.get_episodes(term))
echel0n's avatar
echel0n committed
570

571

echel0n's avatar
echel0n committed
572
@Route('/browser(/?.*)')
573
class WebFileBrowser(WebHandler):
574
575
576
    def __init__(self, *args, **kwargs):
        super(WebFileBrowser, self).__init__(*args, **kwargs)

echel0n's avatar
echel0n committed
577
    def index(self, path='', includeFiles=False, fileTypes=''):
echel0n's avatar
echel0n committed
578
        self.set_header('Content-Type', 'application/json')
echel0n's avatar
echel0n committed
579
        return json_encode(foldersAtPath(path, True, bool(int(includeFiles)), fileTypes.split(',')))
echel0n's avatar
echel0n committed
580

echel0n's avatar
echel0n committed
581
    def complete(self, term, includeFiles=False, fileTypes=''):
echel0n's avatar
echel0n committed
582
        self.set_header('Content-Type', 'application/json')
echel0n's avatar
echel0n committed
583
584
585
586
587
        return json_encode([entry['path'] for entry in foldersAtPath(
            os.path.dirname(term),
            includeFiles=bool(int(includeFiles)),
            fileTypes=fileTypes.split(',')
        ) if 'path' in entry])
echel0n's avatar
echel0n committed
588

589

echel0n's avatar
echel0n committed
590
@Route('/home(/?.*)')
591
class Home(WebHandler):
592
593
594
    def __init__(self, *args, **kwargs):
        super(Home, self).__init__(*args, **kwargs)

Dustyn Gibson's avatar
Dustyn Gibson committed
595
596
    @staticmethod
    def _getEpisode(show, season=None, episode=None, absolute=None):
597
        if show is None:
echel0n's avatar
echel0n committed
598
            return _("Invalid show parameters")
599

600
        showObj = findCertainShow(int(show))
601
602

        if showObj is None:
echel0n's avatar
echel0n committed
603
            return _("Invalid show paramaters")
604
605
606
607
608
609

        if absolute:
            epObj = showObj.getEpisode(absolute_number=int(absolute))
        elif season and episode:
            epObj = showObj.getEpisode(int(season), int(episode))
        else:
echel0n's avatar
echel0n committed
610
            return _("Invalid paramaters")
611
612

        if epObj is None:
echel0n's avatar
echel0n committed
613
            return _("Episode couldn't be retrieved")
614
615
616

        return epObj

617
    def index(self):
618
        if not len(sickrage.app.showlist):
echel0n's avatar
echel0n committed
619
620
            return self.redirect('/home/addShows/')

621
        showlists = OrderedDict({'Shows': []})
622
        if sickrage.app.config.anime_split_home:
623
            for show in sickrage.app.showlist:
624
                if show.is_anime:
625
626
627
                    if not showlists.has_key('Anime'):
                        showlists['Anime'] = []
                    showlists['Anime'] += [show]
628
                else:
629
                    showlists['Shows'] += [show]
630
        else:
631
            showlists['Shows'] = sickrage.app.showlist
632

633
        app_stats = app_statistics()
634
635
636
637
638
639
        return self.render(
            "/home/index.mako",
            title="Home",
            header="Show List",
            topmenu="home",
            showlists=showlists,
640
641
642
            show_stat=app_stats[0],
            overall_stats=app_stats[1],
            max_download_count=app_stats[2],
643
644
645
            controller='home',
            action='index'
        )
646

647
    def is_alive(self, *args, **kwargs):
echel0n's avatar
echel0n committed
648
649
        self.set_header('Content-Type', 'text/javascript')

echel0n's avatar
echel0n committed
650
        if not all([kwargs.get('srcallback'), kwargs.get('_')]):
echel0n's avatar
echel0n committed
651
            return _("Error: Unsupported Request. Send jsonp request with 'srcallback' variable in the query string.")
652

653
        if sickrage.app.started:
654
            return "%s({'msg':%s})" % (kwargs['srcallback'], str(sickrage.app.pid))
echel0n's avatar
echel0n committed
655
656
        else:
            return "%s({'msg':%s})" % (kwargs['srcallback'], "nope")
657

Dustyn Gibson's avatar
Dustyn Gibson committed
658
659
    @staticmethod
    def haveKODI():
660
        return sickrage.app.config.use_kodi and sickrage.app.config.kodi_update_library
661

Dustyn Gibson's avatar
Dustyn Gibson committed
662
663
    @staticmethod
    def havePLEX():
664
        return sickrage.app.config.use_plex and sickrage.app.config.plex_update_library
665

Dustyn Gibson's avatar
Dustyn Gibson committed
666
667
    @staticmethod
    def haveEMBY():
668
        return sickrage.app.config.use_emby
josh4trunks's avatar
josh4trunks committed
669

Dustyn Gibson's avatar
Dustyn Gibson committed
670
671
    @staticmethod
    def haveTORRENT():
672
673
674
        if sickrage.app.config.use_torrents and sickrage.app.config.torrent_method != 'blackhole' and \
                (sickrage.app.config.enable_https and sickrage.app.config.torrent_host[:5] == 'https' or not
                sickrage.app.config.enable_https and sickrage.app.config.torrent_host[:5] == 'http:'):
675
676
677
678
            return True
        else:
            return False

Dustyn Gibson's avatar
Dustyn Gibson committed
679
680
    @staticmethod
    def testSABnzbd(host=None, username=None, password=None, apikey=None):
681
        host = clean_url(host)
682

683
        connection, accesMsg = SabNZBd.getSabAccesMethod(host)
684
        if connection:
echel0n's avatar
echel0n committed
685
            authed, authMsg = SabNZBd.test_authentication(host, username, password, apikey)
686
            if authed:
echel0n's avatar
echel0n committed
687
                return _('Success. Connected and authenticated')
688
            else:
echel0n's avatar
echel0n committed
689
690
                return _('Authentication failed. SABnzbd expects ') + accesMsg + _(
                    ' as authentication method, ') + authMsg
691
        else:
echel0n's avatar
echel0n committed
692
            return _('Unable to connect to host')
693

Dustyn Gibson's avatar
Dustyn Gibson committed
694
695
    @staticmethod
    def testTorrent(torrent_method=None, host=None, username=None, password=None):
696

697
        host = clean_url(host)
698

699
        client = getClientIstance(torrent_method)
700

echel0n's avatar
echel0n committed
701
        __, accesMsg = client(host, username, password).test_authentication()
702

703
        return accesMsg
704

Dustyn Gibson's avatar
Dustyn Gibson committed
705
706
    @staticmethod
    def testFreeMobile(freemobile_id=None, freemobile_apikey=None):
Heisenberg74's avatar
Heisenberg74 committed
707

708
        result, message = sickrage.app.notifier_providers['freemobile'].test_notify(freemobile_id, freemobile_apikey)
Heisenberg74's avatar
Heisenberg74 committed
709
        if result:
echel0n's avatar
echel0n committed
710
            return _('SMS sent successfully')
Heisenberg74's avatar
Heisenberg74 committed
711
        else:
echel0n's avatar
echel0n committed
712
            return _('Problem sending SMS: ') + message
713

echel0n's avatar
echel0n committed
714
715
716
    @staticmethod
    def testTelegram(telegram_id=None, telegram_apikey=None):

717
        result, message = sickrage.app.notifier_providers['telegram'].test_notify(telegram_id, telegram_apikey)
echel0n's avatar
echel0n committed
718
        if result:
echel0n's avatar
echel0n committed
719
            return _('Telegram notification succeeded. Check your Telegram clients to make sure it worked')
echel0n's avatar
echel0n committed
720
        else:
echel0n's avatar
echel0n committed