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, 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, google_drive
50
from sickrage.core.blackandwhitelist import BlackAndWhiteList, \
51
    short_group_names
52
from sickrage.core.classes import ErrorViewer, AllShowsUI
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
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
    def prepare(self):
110
111
        if not self.request.full_url().startswith(sickrage.app.config.web_root):
            self.redirect("{}{}".format(sickrage.app.config.web_root, self.request.full_url()))
112

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

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

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

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

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

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

161
    def set_current_user(self, user, remember_me=False):
162
        self.set_secure_cookie('sickrage_user', user, expires_days=30 if remember_me else None)
163

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

167
    def clear_current_user(self):
168
        self.clear_cookie('sickrage_user')
169

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

198
        template_kwargs.update(self.get_template_namespace())
199
        template_kwargs.update(kwargs)
200

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

210
211
    def render(self, template_name, **kwargs):
        return self.render_string(template_name, **kwargs)
212

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

221
        return function(**kwargs)
222

echel0n's avatar
echel0n committed
223
224
225
226
227
    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')
228

229

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

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

243
        if method:
echel0n's avatar
echel0n committed
244
245
            result = yield self.route(method, **self.request.arguments)
            self.finish(result)
246

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

257

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

echel0n's avatar
echel0n committed
262
263
    def get(self, *args, **kwargs):
        redirect_uri = "{}://{}/login".format(self.request.protocol, self.request.host)
264

echel0n's avatar
echel0n committed
265
266
        code = self.get_argument('code', False)
        if code:
267
268
269
270
271
272
273
274
            API().token = sickrage.app.oidc_client.authorization_code(code, redirect_uri)

            try:
                API().register_appid(sickrage.app.config.app_id)
                self.set_current_user(json_encode(API().userinfo), True)
            except Exception:
                return self.redirect('/logout')

echel0n's avatar
echel0n committed
275
276
            redirect_page = self.get_argument('next', "/{}/".format(sickrage.app.config.default_page))
            return self.redirect("{}".format(redirect_page))
echel0n's avatar
echel0n committed
277
        else:
echel0n's avatar
echel0n committed
278
279
            authorization_url = sickrage.app.oidc_client.authorization_url(scope='offline_access',
                                                                           redirect_uri=redirect_uri)
echel0n's avatar
echel0n committed
280
            self.redirect(authorization_url)
echel0n's avatar
echel0n committed
281

282

echel0n's avatar
echel0n committed
283
class LogoutHandler(BaseHandler):
284
    def __init__(self, *args, **kwargs):
285
        super(LogoutHandler, self).__init__(*args, **kwargs)
286

287
    def prepare(self, *args, **kwargs):
288
289
        if API().token:
            sickrage.app.oidc_client.logout(API().token['refresh_token'])
echel0n's avatar
echel0n committed
290

291
        self.clear_current_user()
292
        return self.redirect('/login/')
293

294

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

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

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

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

335
336
                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
337
338
339

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

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

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

        return ical

echel0n's avatar
echel0n committed
368

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

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

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

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

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

Gaëtan Muller's avatar
Gaëtan Muller committed
394
        episodes = {}
395

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

399
400
            if result['showid'] not in episodes:
                episodes[result['showid']] = {}
401

402
403
            if result['season'] not in episodes[result['showid']]:
                episodes[result['showid']][result['season']] = []
404

405
            episodes[result['showid']][result['season']].append(result['episode'])
406

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

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

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

428
        sickrage.app.config.home_layout = layout
429

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

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

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

439
        sickrage.app.config.poster_sortby = sort
440
        sickrage.app.config.save()
441

Dustyn Gibson's avatar
Dustyn Gibson committed
442
443
    @staticmethod
    def setPosterSortDir(direction):
444

445
        sickrage.app.config.poster_sortdir = int(direction)
446
        sickrage.app.config.save()
447

448
449
450
451
452
    def setHistoryLayout(self, layout):

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

453
        sickrage.app.config.history_layout = layout
454

455
        return self.redirect("/history/")
456
457
458

    def toggleDisplayShowSpecials(self, show):

459
        sickrage.app.config.display_show_specials = not sickrage.app.config.display_show_specials
460

461
        return self.redirect("/home/displayShow?show=" + show)
462

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

467
        if layout == 'calendar':
468
            sickrage.app.config.coming_eps_sort = 'date'
469

470
        sickrage.app.config.coming_eps_layout = layout
471

472
        return self.redirect("/schedule/")
473

474
    def toggleScheduleDisplayPaused(self):
475

476
        sickrage.app.config.coming_eps_display_paused = not sickrage.app.config.coming_eps_display_paused
477

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

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

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

487
        sickrage.app.config.coming_eps_sort = sort
488

489
        return self.redirect("/schedule/")
490

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

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

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

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

522

echel0n's avatar
echel0n committed
523
@Route('/ui(/?.*)')
524
class UI(WebHandler):
525
526
    def __init__(self, *args, **kwargs):
        super(UI, self).__init__(*args, **kwargs)
echel0n's avatar
echel0n committed
527
        self.set_header('Content-Type', 'application/json')
528

Dustyn Gibson's avatar
Dustyn Gibson committed
529
530
    @staticmethod
    def add_message():
531
532
        sickrage.app.alerts.message('Test 1', 'This is test number 1')
        sickrage.app.alerts.error('Test 2', 'This is test number 2')
533
534
        return "ok"

535
    def get_messages(self):
536
        messages = {}
537
        cur_notification_num = 0
538
        for cur_notification in sickrage.app.alerts.get_notifications(self.request.remote_ip):
539
            cur_notification_num += 1
540
541
            messages['notification-{}'.format(cur_notification_num)] = {
                'title': cur_notification.title,
542
                'message': cur_notification.message or "",
543
544
                'type': cur_notification.type
            }
545

546
        if messages:
547
            return json_encode(messages)
548

549

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

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

echel0n's avatar
echel0n committed
559
    def complete(self, term, includeFiles=False, fileTypes=''):
echel0n's avatar
echel0n committed
560
        self.set_header('Content-Type', 'application/json')
echel0n's avatar
echel0n committed
561
562
563
564
565
        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
566

567

echel0n's avatar
echel0n committed
568
@Route('/home(/?.*)')
569
class Home(WebHandler):
570
571
572
    def __init__(self, *args, **kwargs):
        super(Home, self).__init__(*args, **kwargs)

Dustyn Gibson's avatar
Dustyn Gibson committed
573
574
    @staticmethod
    def _getEpisode(show, season=None, episode=None, absolute=None):
575
        if show is None:
echel0n's avatar
echel0n committed
576
            return _("Invalid show parameters")
577

578
        showObj = findCertainShow(int(show))
579
580

        if showObj is None:
echel0n's avatar
echel0n committed
581
            return _("Invalid show paramaters")
582
583
584
585
586
587

        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
588
            return _("Invalid paramaters")
589
590

        if epObj is None:
echel0n's avatar
echel0n committed
591
            return _("Episode couldn't be retrieved")
592
593
594

        return epObj

595
    def index(self):
596
        if not len(sickrage.app.showlist):
echel0n's avatar
echel0n committed
597
598
            return self.redirect('/home/addShows/')

599
        showlists = OrderedDict({'Shows': []})
600
        if sickrage.app.config.anime_split_home:
601
            for show in sickrage.app.showlist:
602
                if show.is_anime:
603
604
605
                    if not showlists.has_key('Anime'):
                        showlists['Anime'] = []
                    showlists['Anime'] += [show]
606
                else:
607
                    showlists['Shows'] += [show]
608
        else:
609
            showlists['Shows'] = sickrage.app.showlist
610

611
        show_stats = self.show_statistics()
612
613
614
615
616
617
        return self.render(
            "/home/index.mako",
            title="Home",
            header="Show List",
            topmenu="home",
            showlists=showlists,
618
619
620
            show_stat=show_stats[0],
            max_download_count=show_stats[1],
            overall_stats=overall_stats(),
621
622
623
            controller='home',
            action='index'
        )
624
625
626

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

629
        today = datetime.date.today().toordinal()
630

631
        status_quality = Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.SNATCHED_BEST
echel0n's avatar
echel0n committed
632
        status_download = Quality.DOWNLOADED + Quality.ARCHIVED
633
634

        max_download_count = 1000
echel0n's avatar
echel0n committed
635

636
        for epData in sickrage.app.main_db.all('tv_episodes'):
637
            showid = epData['showid']
echel0n's avatar
echel0n committed
638
639
640
641
642
643
644
645
            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

646
647
648
649
            season = epData['season']
            episode = epData['episode']
            airdate = epData['airdate']
            status = epData['status']
echel0n's avatar
echel0n committed
650
651
652
653
654

            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]
655
                ) or (status in status_quality + status_download): show_stat[showid]['ep_total'] += 1
echel0n's avatar
echel0n committed
656

657
658
659
                if show_stat[showid]['ep_total'] > max_download_count:
                    max_download_count = show_stat[showid]['ep_total']

660
                if airdate >= today and status in [WANTED, UNAIRED] and not show_stat[showid]['ep_airs_next']:
echel0n's avatar
echel0n committed
661
                    show_stat[showid]['ep_airs_next'] = airdate
662
                elif airdate < today > show_stat[showid]['ep_airs_prev'] and status != UNAIRED:
echel0n's avatar
echel0n committed
663
664
                    show_stat[showid]['ep_airs_prev'] = airdate

665
666
667
668
        max_download_count *= 100

        return show_stat, max_download_count

669
    def is_alive(self, *args, **kwargs):
echel0n's avatar
echel0n committed
670
671
        self.set_header('Content-Type', 'text/javascript')

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

675
        if sickrage.app.started:
676
            return "%s({'msg':%s})" % (kwargs['srcallback'], str(sickrage.app.pid))
echel0n's avatar
echel0n committed
677
678
        else:
            return "%s({'msg':%s})" % (kwargs['srcallback'], "nope")
679

Dustyn Gibson's avatar
Dustyn Gibson committed
680
681
    @staticmethod
    def haveKODI():
682
        return sickrage.app.config.use_kodi and sickrage.app.config.kodi_update_library
683

Dustyn Gibson's avatar
Dustyn Gibson committed
684
685
    @staticmethod
    def havePLEX():
686
        return sickrage.app.config.use_plex and sickrage.app.config.plex_update_library
687

Dustyn Gibson's avatar
Dustyn Gibson committed
688
689
    @staticmethod
    def haveEMBY():
690
        return sickrage.app.config.use_emby
josh4trunks's avatar
josh4trunks committed
691

Dustyn Gibson's avatar
Dustyn Gibson committed
692
693
    @staticmethod
    def haveTORRENT():
694
695
696
        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:'):
697
698
699
700
            return True
        else:
            return False

Dustyn Gibson's avatar
Dustyn Gibson committed