views.py 211 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

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

import sickrage
echel0n's avatar
echel0n committed
45
import sickrage.subtitles
echel0n's avatar
echel0n committed
46
from adba import aniDBAbstracter
47
from sickrage.clients import getClientIstance
48
from sickrage.clients.sabnzbd import SabNZBd
49
from sickrage.core import API
50
from sickrage.core.blackandwhitelist import BlackAndWhiteList, \
51
    short_group_names
echel0n's avatar
v9.0.35    
echel0n committed
52
from sickrage.core.classes import ErrorViewer, AllShowsUI, AttrDict
53
54
from sickrage.core.classes import WarningViewer
from sickrage.core.common import FAILED, IGNORED, Overview, Quality, SKIPPED, \
55
    SNATCHED, UNAIRED, WANTED, cpu_presets, statusStrings
56
from sickrage.core.exceptions import CantRefreshShowException, \
57
    CantUpdateShowException, EpisodeDeletedException, \
echel0n's avatar
echel0n committed
58
    NoNFOException, CantRemoveShowException
echel0n's avatar
echel0n committed
59
60
from sickrage.core.helpers import argToBool, backupSR, chmodAsParent, findCertainShow, generateApiKey, \
    getDiskSpaceUsage, makeDir, readFileBuffered, \
61
    remove_article, restoreConfigZip, \
62
    sanitizeFileName, clean_url, try_int, torrent_webui_url, checkbox_to_value, clean_host, \
63
    clean_hosts, overall_stats
64
from sickrage.core.helpers.browser import foldersAtPath
65
from sickrage.core.helpers.compat import cmp
66
from sickrage.core.helpers.srdatetime import srDateTime
67
from sickrage.core.imdb_popular import imdbPopular
echel0n's avatar
echel0n committed
68
from sickrage.core.media.util import indexerImage
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
76
    get_scene_absolute_numbering_for_show, get_scene_numbering, \
    get_scene_numbering_for_show, get_xem_absolute_numbering_for_show, \
    get_xem_numbering_for_show, set_scene_numbering, xem_refresh
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
echel0n's avatar
echel0n committed
84
from sickrage.core.websession import WebSession
85
from sickrage.indexers import IndexerApi
86
from sickrage.notifiers import Notifiers
87
from sickrage.providers import NewznabProvider, TorrentRssProvider
88

echel0n's avatar
echel0n committed
89

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

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

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

111
    def prepare(self):
112
113
        if not self.request.full_url().startswith(sickrage.app.config.web_root):
            self.redirect("{}{}".format(sickrage.app.config.web_root, self.request.full_url()))
114

115
116
117
    def write_error(self, status_code, **kwargs):
        # handle 404 http errors
        if status_code == 404:
118
            url = self.request.uri
119
120
            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:]
121

122
            if url[:3] != 'api':
123
124
                return self.finish(self.render(
                    '/errors/404.mako',
echel0n's avatar
echel0n committed
125
126
                    title=_('HTTP Error 404'),
                    header=_('HTTP Error 404'))
127
                )
128
            else:
129
                self.write('Wrong API key used')
130

131
        elif self.settings.get("debug") and "exc_info" in kwargs:
132
            exc_info = kwargs["exc_info"]
133
134
            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
135
136
137
138
                                    self.request.__dict__.keys()])
            error = exc_info[1]

            self.set_header('Content-Type', 'text/html')
139
            self.write("""<html>
140
                                 <title>{error}</title>
141
                                 <body>
142
143
                                    <button onclick="window.location='{webroot}/logs/';">View Log(Errors)</button>
                                    <button onclick="window.location='{webroot}/home/restart?force=1';">Restart SiCKRAGE</button>
144
                                    <h2>Error</h2>
145
                                    <p>{error}</p>
146
                                    <h2>Traceback</h2>
147
                                    <p>{traceback}</p>
148
                                    <h2>Request Info</h2>
149
                                    <p>{request}</p>
150
                                 </body>
151
152
153
                               </html>""".format(error=error,
                                                 traceback=trace_info,
                                                 request=request_info,
154
                                                 webroot=sickrage.app.config.web_root))
155

156
    def redirect(self, url, permanent=False, status=None):
157
158
        if not url.startswith(sickrage.app.config.web_root):
            url = sickrage.app.config.web_root + url
159
160
161
162
            permanent = True

        super(BaseHandler, self).redirect(url, permanent, status)

Dustyn Gibson's avatar
Dustyn Gibson committed
163
    def get_current_user(self):
164
        return self.get_secure_cookie('user')
echel0n's avatar
echel0n committed
165

166
    def render_string(self, template_name, **kwargs):
167
        template_kwargs = {
168
169
170
171
            'title': "",
            'header': "",
            'topmenu': "",
            'submenu': "",
172
173
            'controller': "home",
            'action': "index",
174
            'srPID': sickrage.app.pid,
175
            'srHttpsEnabled': sickrage.app.config.enable_https or bool(
echel0n's avatar
echel0n committed
176
                self.request.headers.get('X-Forwarded-Proto') == 'https'),
177
            'srHost': self.request.headers.get('X-Forwarded-Host', self.request.host.split(':')[0]),
178
179
180
181
182
183
            '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,
184
185
186
            'numErrors': len(ErrorViewer.errors),
            'numWarnings': len(WarningViewer.errors),
            'srStartTime': self.startTime,
187
            'makoStartTime': time.time(),
echel0n's avatar
echel0n committed
188
            'overall_stats': None,
echel0n's avatar
echel0n committed
189
            'torrent_webui_url': torrent_webui_url(),
190
            'application': self.application,
echel0n's avatar
echel0n committed
191
            'request': self.request,
192
193
        }

194
        template_kwargs.update(self.get_template_namespace())
195
        template_kwargs.update(kwargs)
196

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

206
207
    def render(self, template_name, **kwargs):
        return self.render_string(template_name, **kwargs)
208

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

217
        return function(**kwargs)
218

echel0n's avatar
echel0n committed
219
220
221
222
223
    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')
224

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

echel0n's avatar
echel0n committed
229
    @coroutine
230
    @authenticated
231
    def prepare(self, *args, **kwargs):
232
        # route -> method obj
233
234
        method = getattr(
            self, self.request.path.strip('/').split('/')[::-1][0].replace('.', '_'),
235
            getattr(self, 'index', None)
236
        )
echel0n's avatar
echel0n committed
237

238
        if method:
echel0n's avatar
echel0n committed
239
240
            result = yield self.route(method, **self.request.arguments)
            self.finish(result)
241

242

echel0n's avatar
echel0n committed
243
class LoginHandler(BaseHandler):
244
245
246
    def __init__(self, *args, **kwargs):
        super(LoginHandler, self).__init__(*args, **kwargs)

247
    def prepare(self, *args, **kwargs):
echel0n's avatar
echel0n committed
248
        self.finish(self.auth())
echel0n's avatar
echel0n committed
249

echel0n's avatar
echel0n committed
250
    def auth(self):
echel0n's avatar
echel0n committed
251
        if sickrage.app.developer:
252
253
            self.set_secure_cookie('user', json_encode(sickrage.app.config.api_key))

echel0n's avatar
echel0n committed
254
255
        if self.get_current_user():
            return self.redirect("/{}/".format(sickrage.app.config.default_page))
256

echel0n's avatar
echel0n committed
257
258
        username = self.get_argument('username', '')
        password = self.get_argument('password', '')
259

260
        if username == sickrage.app.config.web_username and password == sickrage.app.config.web_password:
261
            Notifiers.notify_login(self.request.remote_ip)
262

263
            remember_me = int(self.get_argument('remember_me', default=0))
echel0n's avatar
echel0n committed
264

265
            self.set_secure_cookie('user',
266
                                   json_encode(sickrage.app.config.api_key),
267
268
                                   expires_days=30 if remember_me > 0 else None)

269
            sickrage.app.log.debug('User logged into the SiCKRAGE web interface')
270

271
            return self.redirect("/{}/".format(sickrage.app.config.default_page))
echel0n's avatar
echel0n committed
272
        elif username and password:
273
            sickrage.app.log.warning(
274
275
                'User attempted a failed login to the SiCKRAGE web interface from IP: {}'.format(
                    self.request.remote_ip)
276
            )
277
278
279
280
281
282
283
284
285

        return self.render(
            "/login.mako",
            title="Login",
            header="Login",
            topmenu="login",
            controller='root',
            action='login'
        )
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):
293
        self.clear_cookie("user")
294
        return self.redirect('/login/')
295

296

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

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

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

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

337
338
                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
339
340
341

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

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

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

        return ical

echel0n's avatar
echel0n committed
370

371
@Route('(.*)(/?.*)')
372
class WebRoot(WebHandler):
373
374
375
    def __init__(self, *args, **kwargs):
        super(WebRoot, self).__init__(*args, **kwargs)

376
    def index(self):
377
        return self.redirect('/' + sickrage.app.config.default_page + '/')
378

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

384
385
    def messages_json(self):
        """ Get /sickrage/locale/{lang_code}/LC_MESSAGES/messages.json """
386
        locale_file = os.path.join(sickrage.LOCALE_DIR, sickrage.app.config.gui_lang, 'LC_MESSAGES/messages.json')
387
388
        if os.path.isfile(locale_file):
            self.set_header('Content-Type', 'application/json')
389
            with io.open(locale_file, 'r', encoding='utf8') as f:
390
391
                return f.read()

392
    def apibuilder(self):
393
        def titler(x):
394
            return (remove_article(x), x)[not x or sickrage.app.config.sort_article]
395

Gaëtan Muller's avatar
Gaëtan Muller committed
396
        episodes = {}
397

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

401
402
            if result['showid'] not in episodes:
                episodes[result['showid']] = {}
403

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

407
            episodes[result['showid']][result['season']].append(result['episode'])
408

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

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

426
    def setHomeLayout(self, layout):
Goeny's avatar
Goeny committed
427
        if layout not in ('poster', 'small', 'banner', 'simple', 'coverflow'):
428
429
            layout = 'poster'

430
        sickrage.app.config.home_layout = layout
431

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

Dustyn Gibson's avatar
Dustyn Gibson committed
435
436
    @staticmethod
    def setPosterSortBy(sort):
437
438
439
440

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

441
        sickrage.app.config.poster_sortby = sort
442
        sickrage.app.config.save()
443

Dustyn Gibson's avatar
Dustyn Gibson committed
444
445
    @staticmethod
    def setPosterSortDir(direction):
446

447
        sickrage.app.config.poster_sortdir = int(direction)
448
        sickrage.app.config.save()
449

450
451
452
453
454
    def setHistoryLayout(self, layout):

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

455
        sickrage.app.config.history_layout = layout
456

457
        return self.redirect("/history/")
458
459
460

    def toggleDisplayShowSpecials(self, show):

461
        sickrage.app.config.display_show_specials = not sickrage.app.config.display_show_specials
462

463
        return self.redirect("/home/displayShow?show=" + show)
464

465
    def setScheduleLayout(self, layout):
466
        if layout not in ('poster', 'banner', 'list', 'calendar'):
467
468
            layout = 'banner'

469
        if layout == 'calendar':
470
            sickrage.app.config.coming_eps_sort = 'date'
471

472
        sickrage.app.config.coming_eps_layout = layout
473

474
        return self.redirect("/schedule/")
475

476
    def toggleScheduleDisplayPaused(self):
477

478
        sickrage.app.config.coming_eps_display_paused = not sickrage.app.config.coming_eps_display_paused
479

480
        return self.redirect("/schedule/")
481

482
    def setScheduleSort(self, sort):
483
484
        if sort not in ('date', 'network', 'show'):
            sort = 'date'
485

486
        if sickrage.app.config.coming_eps_layout == 'calendar':
echel0n's avatar
echel0n committed
487
            sort = 'date'
488

489
        sickrage.app.config.coming_eps_sort = sort
490

491
        return self.redirect("/schedule/")
492

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

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

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

echel0n's avatar
echel0n committed
521
    def getIndexerImage(self, indexerid):
522
        return indexerImage(id=indexerid, which="poster_thumb")
echel0n's avatar
echel0n committed
523

524

echel0n's avatar
echel0n committed
525
@Route('/google(/?.*)')
526
class GoogleAuth(WebHandler):
echel0n's avatar
echel0n committed
527
528
    def __init__(self, *args, **kwargs):
        super(GoogleAuth, self).__init__(*args, **kwargs)
529

530
    def get_user_code(self):
531
        data = sickrage.app.google_auth.get_user_code()
532
        return json_encode({field: str(getattr(data, field)) for field in data._fields})
533

534
    def get_credentials(self, flow_info):
echel0n's avatar
echel0n committed
535
        try:
536
            data = sickrage.app.google_auth.get_credentials(AttrDict(json_decode(flow_info)))
537
538
539
            return json_encode(data.token_response)
        except Exception as e:
            return json_encode({'error': e.message})
540

541
    def refresh_credentials(self):
542
        sickrage.app.google_auth.refresh_credentials()
543

echel0n's avatar
echel0n committed
544
    def logout(self):
545
        sickrage.app.google_auth.logout()
546

547

echel0n's avatar
echel0n committed
548
@Route('/ui(/?.*)')
549
class UI(WebHandler):
550
551
    def __init__(self, *args, **kwargs):
        super(UI, self).__init__(*args, **kwargs)
echel0n's avatar
echel0n committed
552
        self.set_header('Content-Type', 'application/json')
553

Dustyn Gibson's avatar
Dustyn Gibson committed
554
555
    @staticmethod
    def add_message():
556
557
        sickrage.app.alerts.message('Test 1', 'This is test number 1')
        sickrage.app.alerts.error('Test 2', 'This is test number 2')
558
559
        return "ok"

560
    def get_messages(self):
561
        messages = {}
562
        cur_notification_num = 0
563
        for cur_notification in sickrage.app.alerts.get_notifications(self.request.remote_ip):
564
            cur_notification_num += 1
565
566
            messages['notification-{}'.format(cur_notification_num)] = {
                'title': cur_notification.title,
567
                'message': cur_notification.message or "",
568
569
                'type': cur_notification.type
            }
570

571
        if messages:
572
            return json_encode(messages)
573

574

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

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

echel0n's avatar
echel0n committed
584
    def complete(self, term, includeFiles=False, fileTypes=''):
echel0n's avatar
echel0n committed
585
        self.set_header('Content-Type', 'application/json')
echel0n's avatar
echel0n committed
586
587
588
589
590
        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
591

592

echel0n's avatar
echel0n committed
593
@Route('/home(/?.*)')
594
class Home(WebHandler):
595
596
597
598
    def __init__(self, *args, **kwargs):
        super(Home, self).__init__(*args, **kwargs)

    def _genericMessage(self, subject, message):
599
600
601
602
603
604
605
606
607
        return self.render(
            "/generic_message.mako",
            message=message,
            subject=subject,
            topmenu="home",
            title="",
            controller='home',
            action='genericmessage'
        )
608

Dustyn Gibson's avatar
Dustyn Gibson committed
609
610
    @staticmethod
    def _getEpisode(show, season=None, episode=None, absolute=None):
611
        if show is None:
echel0n's avatar
echel0n committed
612
            return _("Invalid show parameters")
613

614
        showObj = findCertainShow(int(show))
615
616

        if showObj is None:
echel0n's avatar
echel0n committed
617
            return _("Invalid show paramaters")
618
619
620
621
622
623

        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
624
            return _("Invalid paramaters")
625
626

        if epObj is None:
echel0n's avatar
echel0n committed
627
            return _("Episode couldn't be retrieved")
628
629
630

        return epObj

631
    def index(self):
632
        if not len(sickrage.app.showlist):
echel0n's avatar
echel0n committed
633
634
            return self.redirect('/home/addShows/')

635
        showlists = OrderedDict({'Shows': []})
636
        if sickrage.app.config.anime_split_home:
637
            for show in sickrage.app.showlist:
638
                if show.is_anime:
639
640
641
                    if not showlists.has_key('Anime'):
                        showlists['Anime'] = []
                    showlists['Anime'] += [show]
642
                else:
643
                    showlists['Shows'] += [show]
644
        else:
645
            showlists['Shows'] = sickrage.app.showlist
646

647
        show_stats = self.show_statistics()
648
649
650
651
652
653
        return self.render(
            "/home/index.mako",
            title="Home",
            header="Show List",
            topmenu="home",
            showlists=showlists,
654
655
656
            show_stat=show_stats[0],
            max_download_count=show_stats[1],
            overall_stats=overall_stats(),
657
658
659
            controller='home',
            action='index'
        )
660
661
662

    @staticmethod
    def show_statistics():
echel0n's avatar
echel0n committed
663
        show_stat = {}
664

665
        today = datetime.date.today().toordinal()
666

667
        status_quality = Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST
echel0n's avatar
echel0n committed
668
        status_download = Quality.DOWNLOADED + Quality.ARCHIVED
669
670

        max_download_count = 1000
echel0n's avatar
echel0n committed
671

672
        for epData in sickrage.app.main_db.all('tv_episodes'):
673
            showid = epData['showid']
echel0n's avatar
echel0n committed
674
675
676
677
678
679
680
681
            if showid not in show_stat:
                show_stat[showid] = {}
                show_stat[showid]['ep_snatched'] = 0
                show_stat[showid]['ep_downloaded'] = 0
                show_stat[showid]['ep_total'] = 0
                show_stat[showid]['ep_airs_next'] = None
                show_stat[showid]['ep_airs_prev'] = None

682
683
684
685
            season = epData['season']
            episode = epData['episode']
            airdate = epData['airdate']
            status = epData['status']
echel0n's avatar
echel0n committed
686
687
688
689
690

            if season > 0 and episode > 0 and airdate > 1:
                if status in status_quality: show_stat[showid]['ep_snatched'] += 1
                if status in status_download: show_stat[showid]['ep_downloaded'] += 1
                if (airdate <= today and status in [SKIPPED, WANTED, FAILED]
691
                ) or (status in status_quality + status_download): show_stat[showid]['ep_total'] += 1