views.py 209 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
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
echel0n's avatar
echel0n committed
39
from tornado.concurrent import run_on_executor
echel0n's avatar
echel0n committed
40
from tornado.escape import json_encode, recursive_unicode, json_decode
echel0n's avatar
echel0n committed
41
42
from tornado.gen import coroutine
from tornado.process import cpu_count
43
from tornado.web import RequestHandler, authenticated
44
45

import sickrage
echel0n's avatar
echel0n committed
46
import sickrage.subtitles
echel0n's avatar
echel0n committed
47
from adba import aniDBAbstracter
48
from sickrage.clients import getClientIstance
49
from sickrage.clients.sabnzbd import SabNZBd
50
from sickrage.core import API, google_drive
51
from sickrage.core.blackandwhitelist import BlackAndWhiteList, \
52
    short_group_names
53
from sickrage.core.classes import ErrorViewer, AllShowsUI
54
55
from sickrage.core.classes import WarningViewer
from sickrage.core.common import FAILED, IGNORED, Overview, Quality, SKIPPED, \
56
    SNATCHED, UNAIRED, WANTED, cpu_presets, statusStrings
57
from sickrage.core.exceptions import CantRefreshShowException, \
58
    CantUpdateShowException, EpisodeDeletedException, \
echel0n's avatar
echel0n committed
59
    NoNFOException, CantRemoveShowException
echel0n's avatar
echel0n committed
60
61
from sickrage.core.helpers import argToBool, backupSR, chmodAsParent, findCertainShow, generateApiKey, \
    getDiskSpaceUsage, makeDir, readFileBuffered, \
62
    remove_article, restoreConfigZip, \
63
    sanitizeFileName, clean_url, try_int, torrent_webui_url, checkbox_to_value, clean_host, \
64
    clean_hosts, app_statistics
65
from sickrage.core.helpers.browser import foldersAtPath
66
from sickrage.core.helpers.compat import cmp
67
from sickrage.core.helpers.srdatetime import srDateTime
68
from sickrage.core.imdb_popular import imdbPopular
69
70
from sickrage.core.nameparser import validator
from sickrage.core.queues.search import BacklogQueueItem, FailedQueueItem, \
71
    MANUAL_SEARCH_HISTORY, ManualSearchQueueItem
echel0n's avatar
echel0n committed
72
from sickrage.core.scene_exceptions import get_scene_exceptions, update_scene_exceptions
73
from sickrage.core.scene_numbering import get_scene_absolute_numbering, \
74
75
    get_scene_absolute_numbering_for_show, get_scene_numbering, \
    get_scene_numbering_for_show, get_xem_absolute_numbering_for_show, \
76
    get_xem_numbering_for_show, set_scene_numbering
echel0n's avatar
echel0n committed
77
from sickrage.core.traktapi import srTraktAPI
78
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
from sickrage.core.updaters import tz_updater
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
136
137
138
139
140
141
142
143
144
145
146
                             <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>
                                <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))
147

Dustyn Gibson's avatar
Dustyn Gibson committed
148
    def get_current_user(self):
echel0n's avatar
echel0n committed
149
150
151
        user = self.get_secure_cookie('sr_user')
        if user:
            return json_decode(user)
152

153
    def render_string(self, template_name, **kwargs):
154
        template_kwargs = {
155
156
157
158
            'title': "",
            'header': "",
            'topmenu': "",
            'submenu': "",
159
160
            'controller': "home",
            'action': "index",
161
            'srPID': sickrage.app.pid,
162
            'srHttpsEnabled': sickrage.app.config.enable_https or bool(
echel0n's avatar
echel0n committed
163
                self.request.headers.get('X-Forwarded-Proto') == 'https'),
164
            'srHost': self.request.headers.get('X-Forwarded-Host', self.request.host.split(':')[0]),
165
166
167
168
169
170
            '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,
171
172
            'srLocale': self.get_user_locale().code,
            'srLocaleDir': sickrage.LOCALE_DIR,
173
174
175
            'numErrors': len(ErrorViewer.errors),
            'numWarnings': len(WarningViewer.errors),
            'srStartTime': self.startTime,
176
            'makoStartTime': time.time(),
echel0n's avatar
echel0n committed
177
            'overall_stats': None,
echel0n's avatar
echel0n committed
178
            'torrent_webui_url': torrent_webui_url(),
179
            'application': self.application,
echel0n's avatar
echel0n committed
180
            'request': self.request,
181
182
        }

183
        template_kwargs.update(self.get_template_namespace())
184
        template_kwargs.update(kwargs)
185

186
187
        try:
            return self.mako_lookup.get_template(template_name).render_unicode(**template_kwargs)
188
        except Exception:
echel0n's avatar
echel0n committed
189
190
            kwargs['title'] = _('HTTP Error 500')
            kwargs['header'] = _('HTTP Error 500')
191
192
193
            kwargs['backtrace'] = RichTraceback()
            template_kwargs.update(kwargs)
            return self.mako_lookup.get_template('/errors/500.mako').render_unicode(**template_kwargs)
194

195
196
    def render(self, template_name, **kwargs):
        return self.render_string(template_name, **kwargs)
197

echel0n's avatar
echel0n committed
198
    @run_on_executor
199
    def worker(self, function, **kwargs):
echel0n's avatar
echel0n committed
200
        threading.currentThread().setName("TORNADO")
201
        kwargs = recursive_unicode(kwargs)
202
203
204
        for arg, value in kwargs.items():
            if len(value) == 1:
                kwargs[arg] = value[0]
echel0n's avatar
echel0n committed
205

206
        return function(**kwargs)
207

echel0n's avatar
echel0n committed
208
209
210
211
212
    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')
213

214
215
216
217
218
219
220
221
222
    def redirect(self, url, permanent=True, status=None):
        m = re.search(r'(?!%s)/(.*)' % sickrage.app.config.web_root, url)
        if m: url = "{}/{}".format(sickrage.app.config.web_root, m.group(0).lstrip('/'))
        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()

223

224
class WebHandler(BaseHandler):
225
226
    def __init__(self, *args, **kwargs):
        super(WebHandler, self).__init__(*args, **kwargs)
227

echel0n's avatar
echel0n committed
228
    @coroutine
229
    @authenticated
230
231
232
233
234
235
236
237
238
239
240
    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):
241
        # route -> method obj
242
243
        method = getattr(
            self, self.request.path.strip('/').split('/')[::-1][0].replace('.', '_'),
244
            getattr(self, 'index', None)
245
        )
echel0n's avatar
echel0n committed
246

247
        if method: return self.worker(method, **self.request.arguments)
248

249
250
251
252
253
254
255
256
257
    def _genericMessage(self, subject, message):
        return self.render(
            "/generic_message.mako",
            message=message,
            subject=subject,
            title="",
            controller='root',
            action='genericmessage'
        )
258

259

echel0n's avatar
echel0n committed
260
class LoginHandler(BaseHandler):
261
262
263
    def __init__(self, *args, **kwargs):
        super(LoginHandler, self).__init__(*args, **kwargs)

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

echel0n's avatar
echel0n committed
267
268
        code = self.get_argument('code', False)
        if code:
269
            try:
echel0n's avatar
echel0n committed
270
                API().token = sickrage.app.oidc_client.authorization_code(code, redirect_uri)
271
272
                self.set_secure_cookie('sr_refresh_token', API().token['refresh_token'])

273
                API().register_appid(sickrage.app.config.app_id)
echel0n's avatar
echel0n committed
274

275
276
                user = sickrage.app.oidc_client.userinfo(API().token['access_token'])
                self.set_secure_cookie('sr_user', json_encode(user))
echel0n's avatar
echel0n committed
277
            except Exception as e:
278
279
                return self.redirect('/logout')

echel0n's avatar
echel0n committed
280
281
            redirect_page = self.get_argument('next', "/{}/".format(sickrage.app.config.default_page))
            return self.redirect("{}".format(redirect_page))
echel0n's avatar
echel0n committed
282
        else:
echel0n's avatar
echel0n committed
283
284
            authorization_url = sickrage.app.oidc_client.authorization_url(scope='offline_access',
                                                                           redirect_uri=redirect_uri)
echel0n's avatar
echel0n committed
285
            self.redirect(authorization_url)
echel0n's avatar
echel0n committed
286

287

echel0n's avatar
echel0n committed
288
class LogoutHandler(BaseHandler):
289
    def __init__(self, *args, **kwargs):
290
        super(LogoutHandler, self).__init__(*args, **kwargs)
291

292
    def prepare(self, *args, **kwargs):
echel0n's avatar
echel0n committed
293
294
        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
295

echel0n's avatar
echel0n committed
296
        self.clear_all_cookies()
297
        return self.redirect('/login/')
298

299

echel0n's avatar
echel0n committed
300
301
class CalendarHandler(BaseHandler):
    def prepare(self, *args, **kwargs):
302
        if sickrage.app.config.calendar_unprotected:
echel0n's avatar
echel0n committed
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
            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
319
        utc = dateutil.tz.gettz('GMT')
echel0n's avatar
echel0n committed
320

321
        sickrage.app.log.info("Receiving iCal request from %s" % self.request.remote_ip)
echel0n's avatar
echel0n committed
322
323
324
325
326
327

        # 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
328
        ical += 'PRODID://SiCKRAGE Upcoming Episodes//\r\n'
echel0n's avatar
echel0n committed
329
330
331
332
333
334

        # 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)
335
336
        for show in [x for x in sickrage.app.showlist if
                     x.status.lower() in ['continuing', 'returning series'] and x.paused != 1]:
337
338
            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
339

340
341
                air_date_time = tz_updater.parse_date_time(episode['airdate'], show.airs, show.network).astimezone(utc)
                air_date_time_end = air_date_time + datetime.timedelta(minutes=try_int(show.runtime, 60))
echel0n's avatar
echel0n committed
342
343
344

                # Create event for episode
                ical += 'BEGIN:VEVENT\r\n'
345
346
                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
347
                    "%H%M%S") + 'Z\r\n'
348
                if sickrage.app.config.calendar_icons:
349
                    ical += 'X-GOOGLE-CALENDAR-CONTENT-ICON:https://www.sickrage.ca/favicon.ico\r\n'
echel0n's avatar
echel0n committed
350
351
                    ical += 'X-GOOGLE-CALENDAR-CONTENT-DISPLAY:CHIP\r\n'
                ical += 'SUMMARY: {0} - {1}x{2} - {3}\r\n'.format(
352
                    show.name, episode['season'], episode['episode'], episode['name']
echel0n's avatar
echel0n committed
353
354
                )
                ical += 'UID:SiCKRAGE-' + str(datetime.date.today().isoformat()) + '-' + \
355
                        show.name.replace(" ", "-") + '-E' + str(episode['episode']) + \
echel0n's avatar
echel0n committed
356
357
358
                        'S' + str(episode['season']) + '\r\n'
                if episode['description']:
                    ical += 'DESCRIPTION: {0} on {1} \\n\\n {2}\r\n'.format(
359
360
                        (show.airs or '(Unknown airs)'),
                        (show.network or 'Unknown network'),
echel0n's avatar
echel0n committed
361
362
                        episode['description'].splitlines()[0])
                else:
363
364
                    ical += 'DESCRIPTION:' + (show.airs or '(Unknown airs)') + ' on ' + (
                            show.network or 'Unknown network') + '\r\n'
echel0n's avatar
echel0n committed
365
366
367
368
369
370
371
372

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

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

        return ical

echel0n's avatar
echel0n committed
373

374
@Route('(.*)(/?.*)')
375
class WebRoot(WebHandler):
376
377
378
    def __init__(self, *args, **kwargs):
        super(WebRoot, self).__init__(*args, **kwargs)

379
    def index(self):
echel0n's avatar
echel0n committed
380
        return self.redirect("/{}/".format(sickrage.app.config.default_page))
381

382
    def robots_txt(self):
383
384
        """ Keep web crawlers out """
        self.set_header('Content-Type', 'text/plain')
echel0n's avatar
echel0n committed
385
        return "User-agent: *\nDisallow: /"
386

387
388
389
390
391
392
393
    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()
394

395
    def apibuilder(self):
396
        def titler(x):
397
            return (remove_article(x), x)[not x or sickrage.app.config.sort_article]
398

Gaëtan Muller's avatar
Gaëtan Muller committed
399
        episodes = {}
400

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

404
405
            if result['showid'] not in episodes:
                episodes[result['showid']] = {}
406

407
408
            if result['season'] not in episodes[result['showid']]:
                episodes[result['showid']][result['season']] = []
409

410
            episodes[result['showid']][result['season']].append(result['episode'])
411

412
413
        if len(sickrage.app.config.api_key) == 32:
            apikey = sickrage.app.config.api_key
414
        else:
echel0n's avatar
echel0n committed
415
            apikey = _('API Key not generated')
416

417
418
        return self.render(
            'api_builder.mako',
echel0n's avatar
echel0n committed
419
420
            title=_('API Builder'),
            header=_('API Builder'),
421
            shows=sorted(sickrage.app.showlist, lambda x, y: cmp(titler(x.name), titler(y.name))),
422
423
            episodes=episodes,
            apikey=apikey,
echel0n's avatar
echel0n committed
424
            commands=ApiHandler(self.application, self.request).api_calls,
425
426
427
            controller='root',
            action='api_builder'
        )
428

429
    def setHomeLayout(self, layout):
Goeny's avatar
Goeny committed
430
        if layout not in ('poster', 'small', 'banner', 'simple', 'coverflow'):
431
432
            layout = 'poster'

433
        sickrage.app.config.home_layout = layout
434

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

Dustyn Gibson's avatar
Dustyn Gibson committed
438
439
    @staticmethod
    def setPosterSortBy(sort):
440
441
442
443

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

444
        sickrage.app.config.poster_sortby = sort
445
        sickrage.app.config.save()
446

Dustyn Gibson's avatar
Dustyn Gibson committed
447
448
    @staticmethod
    def setPosterSortDir(direction):
449

450
        sickrage.app.config.poster_sortdir = int(direction)
451
        sickrage.app.config.save()
452

453
454
455
456
457
    def setHistoryLayout(self, layout):

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

458
        sickrage.app.config.history_layout = layout
459

460
        return self.redirect("/history/")
461
462
463

    def toggleDisplayShowSpecials(self, show):

464
        sickrage.app.config.display_show_specials = not sickrage.app.config.display_show_specials
465

466
        return self.redirect("/home/displayShow?show=" + show)
467

468
    def setScheduleLayout(self, layout):
469
        if layout not in ('poster', 'banner', 'list', 'calendar'):
470
471
            layout = 'banner'

472
        if layout == 'calendar':
473
            sickrage.app.config.coming_eps_sort = 'date'
474

475
        sickrage.app.config.coming_eps_layout = layout
476

477
        return self.redirect("/schedule/")
478

479
    def toggleScheduleDisplayPaused(self):
480

481
        sickrage.app.config.coming_eps_display_paused = not sickrage.app.config.coming_eps_display_paused
482

483
        return self.redirect("/schedule/")
484

485
    def setScheduleSort(self, sort):
486
487
        if sort not in ('date', 'network', 'show'):
            sort = 'date'
488

489
        if sickrage.app.config.coming_eps_layout == 'calendar':
echel0n's avatar
echel0n committed
490
            sort = 'date'
491

492
        sickrage.app.config.coming_eps_sort = sort
493

494
        return self.redirect("/schedule/")
495

496
    def schedule(self, layout=None):
497
498
        next_week = datetime.date.today() + datetime.timedelta(days=7)
        next_week1 = datetime.datetime.combine(next_week,
499
                                               datetime.datetime.now().time().replace(tzinfo=sickrage.app.tz))
500
        results = ComingEpisodes.get_coming_episodes(ComingEpisodes.categories,
501
                                                     sickrage.app.config.coming_eps_sort,
echel0n's avatar
echel0n committed
502
                                                     False)
503
        today = datetime.datetime.now().replace(tzinfo=sickrage.app.tz)
504

505
        # Allow local overriding of layout parameter
506
        if layout and layout in ('poster', 'banner', 'list', 'calendar'):
Dustyn Gibson's avatar
Mako    
Dustyn Gibson committed
507
            layout = layout
508
        else:
509
            layout = sickrage.app.config.coming_eps_layout
510

511
512
513
514
515
516
        return self.render(
            'schedule.mako',
            next_week=next_week1,
            today=today,
            results=results,
            layout=layout,
echel0n's avatar
echel0n committed
517
518
            title=_('Schedule'),
            header=_('Schedule'),
519
520
521
522
            topmenu='schedule',
            controller='root',
            action='schedule'
        )
523

echel0n's avatar
echel0n committed
524
525
526
527
    def unlink(self):
        API().unregister_appid(sickrage.app.config.app_id)
        return self.redirect('/logout/')

echel0n's avatar
echel0n committed
528
    def quicksearch_json(self, term):
529
530
        return json_encode(
            sickrage.app.quicksearch_cache.get_shows(term) + sickrage.app.quicksearch_cache.get_episodes(term))
echel0n's avatar
echel0n committed
531

532

echel0n's avatar
echel0n committed
533
@Route('/browser(/?.*)')
534
class WebFileBrowser(WebHandler):
535
536
537
    def __init__(self, *args, **kwargs):
        super(WebFileBrowser, self).__init__(*args, **kwargs)

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

echel0n's avatar
echel0n committed
542
    def complete(self, term, includeFiles=False, fileTypes=''):
echel0n's avatar
echel0n committed
543
        self.set_header('Content-Type', 'application/json')
echel0n's avatar
echel0n committed
544
545
546
547
548
        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
549

550

echel0n's avatar
echel0n committed
551
@Route('/home(/?.*)')
552
class Home(WebHandler):
553
554
555
    def __init__(self, *args, **kwargs):
        super(Home, self).__init__(*args, **kwargs)

Dustyn Gibson's avatar
Dustyn Gibson committed
556
557
    @staticmethod
    def _getEpisode(show, season=None, episode=None, absolute=None):
558
        if show is None:
echel0n's avatar
echel0n committed
559
            return _("Invalid show parameters")
560

561
        showObj = findCertainShow(int(show))
562
563

        if showObj is None:
echel0n's avatar
echel0n committed
564
            return _("Invalid show paramaters")
565
566
567
568
569
570

        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
571
            return _("Invalid paramaters")
572
573

        if epObj is None:
echel0n's avatar
echel0n committed
574
            return _("Episode couldn't be retrieved")
575
576
577

        return epObj

578
    def index(self):
579
        if not len(sickrage.app.showlist):
echel0n's avatar
echel0n committed
580
581
            return self.redirect('/home/addShows/')

582
        showlists = OrderedDict({'Shows': []})
583
        if sickrage.app.config.anime_split_home:
584
            for show in sickrage.app.showlist:
585
                if show.is_anime:
586
587
588
                    if not showlists.has_key('Anime'):
                        showlists['Anime'] = []
                    showlists['Anime'] += [show]
589
                else:
590
                    showlists['Shows'] += [show]
591
        else:
592
            showlists['Shows'] = sickrage.app.showlist
593

594
        app_stats = app_statistics()
595
596
597
598
599
600
        return self.render(
            "/home/index.mako",
            title="Home",
            header="Show List",
            topmenu="home",
            showlists=showlists,
601
602
603
            show_stat=app_stats[0],
            overall_stats=app_stats[1],
            max_download_count=app_stats[2],
604
605
606
            controller='home',
            action='index'
        )
607

608
    def is_alive(self, *args, **kwargs):
echel0n's avatar
echel0n committed
609
610
        self.set_header('Content-Type', 'text/javascript')

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

614
        if sickrage.app.started:
615
            return "%s({'msg':%s})" % (kwargs['srcallback'], str(sickrage.app.pid))
echel0n's avatar
echel0n committed
616
617
        else:
            return "%s({'msg':%s})" % (kwargs['srcallback'], "nope")
618

Dustyn Gibson's avatar
Dustyn Gibson committed
619
620
    @staticmethod
    def haveKODI():
621
        return sickrage.app.config.use_kodi and sickrage.app.config.kodi_update_library
622

Dustyn Gibson's avatar
Dustyn Gibson committed
623
624
    @staticmethod
    def havePLEX():
625
        return sickrage.app.config.use_plex and sickrage.app.config.plex_update_library
626

Dustyn Gibson's avatar
Dustyn Gibson committed
627
628
    @staticmethod
    def haveEMBY():
629
        return sickrage.app.config.use_emby
josh4trunks's avatar
josh4trunks committed
630

Dustyn Gibson's avatar
Dustyn Gibson committed
631
632
    @staticmethod
    def haveTORRENT():
633
634
635
        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:'):
636
637
638
639
            return True
        else:
            return False

Dustyn Gibson's avatar
Dustyn Gibson committed
640
641
    @staticmethod
    def testSABnzbd(host=None, username=None, password=None, apikey=None):
642
        host = clean_url(host)
643

644
        connection, accesMsg = SabNZBd.getSabAccesMethod(host)
645
        if connection:
echel0n's avatar
echel0n committed
646
            authed, authMsg = SabNZBd.test_authentication(host, username, password, apikey)
647
            if authed:
echel0n's avatar
echel0n committed
648
                return _('Success. Connected and authenticated')
649
            else:
echel0n's avatar
echel0n committed
650
651
                return _('Authentication failed. SABnzbd expects ') + accesMsg + _(
                    ' as authentication method, ') + authMsg
652
        else:
echel0n's avatar
echel0n committed
653
            return _('Unable to connect to host')
654

Dustyn Gibson's avatar
Dustyn Gibson committed
655
656
    @staticmethod
    def testTorrent(torrent_method=None, host=None, username=None, password=None):
657

658
        host = clean_url(host)
659

660
        client = getClientIstance(torrent_method)
661

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

664
        return accesMsg
665

Dustyn Gibson's avatar
Dustyn Gibson committed
666
667
    @staticmethod
    def testFreeMobile(freemobile_id=None, freemobile_apikey=None):
Heisenberg74's avatar
Heisenberg74 committed
668

669
        result, message = sickrage.app.notifier_providers['freemobile'].test_notify(freemobile_id, freemobile_apikey)
Heisenberg74's avatar
Heisenberg74 committed
670
        if result:
echel0n's avatar
echel0n committed
671
            return _('SMS sent successfully')
Heisenberg74's avatar
Heisenberg74 committed
672
        else:
echel0n's avatar
echel0n committed
673
            return _('Problem sending SMS: ') + message
674

echel0n's avatar
echel0n committed
675
676
677
    @staticmethod
    def testTelegram(telegram_id=None, telegram_apikey=None):

678
        result, message = sickrage.app.notifier_providers['telegram'].test_notify(telegram_id, telegram_apikey)
echel0n's avatar
echel0n committed
679
        if result:
echel0n's avatar
echel0n committed
680
            return _('Telegram notification succeeded. Check your Telegram clients to make sure it worked')
echel0n's avatar
echel0n committed
681
        else:
echel0n's avatar
echel0n committed
682
            return _('Error sending Telegram notification: {message}').format(message=message)
echel0n's avatar
echel0n committed
683

Dustyn Gibson's avatar
Dustyn Gibson committed
684
685
    @staticmethod
    def testGrowl(host=None, password=None):
686
        host = clean_host(host, default_port=23053)
687

688
        result = sickrage.app.notifier_providers['growl'].test_notify(host, password)
689
690
691
        if password is None or password == '':
            pw_append = ''
        else:
echel0n's avatar
echel0n committed
692
            pw_append = _(' with password: ') + password
693

694
        if result:
echel0n's avatar
echel0n committed
695
            return _('Registered and Tested growl successfully ') + urllib.unquote_plus(host) + pw_append
696
        else:
echel0n's avatar
echel0n committed
697
            return _('Registration and Testing of growl failed ') + urllib.unquote_plus(host) + pw_append
698

Dustyn Gibson's avatar
Dustyn Gibson committed
699
700
    @staticmethod
    def testProwl(prowl_api=None, prowl_priority=0):
echel0n's avatar
echel0n committed
701

702
        result = sickrage.app.notifier_providers['prowl'].test_notify(prowl_api, prowl_priority)
703
        if result:
echel0n's avatar
echel0n committed
704
            return _('Test prowl notice sent successfully')
705
        else:
echel0n's avatar
echel0n committed
706
            return _('Test prowl notice failed')
707

Dustyn Gibson's avatar
Dustyn Gibson committed
708
709
    @staticmethod
    def testBoxcar2(accesstoken=None):