__init__.py 57.7 KB
Newer Older
echel0n's avatar
echel0n committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ##############################################################################
#  Author: echel0n <[email protected]>
#  URL: https://sickrage.ca/
#  Git: https://git.sickrage.ca/SiCKRAGE/sickrage.git
#  -
#  This file is part of SiCKRAGE.
#  -
#  SiCKRAGE is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#  -
#  SiCKRAGE is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  -
#  You should have received a copy of the GNU General Public License
#  along with SiCKRAGE.  If not, see <http://www.gnu.org/licenses/>.
# ##############################################################################
21

22
import datetime
23
24
25
import glob
import os
import re
26
import shutil
27
import stat
28
import threading
29
import time
30
31
32
import traceback

import send2trash
33
import sqlalchemy
34
from adba.aniDBAbstracter import Anime
35
from sqlalchemy import orm
echel0n's avatar
echel0n committed
36
from unidecode import unidecode
echel0n's avatar
v8.8.5    
echel0n committed
37
38

import sickrage
39
from sickrage.core.blackandwhitelist import BlackAndWhiteList
echel0n's avatar
echel0n committed
40
from sickrage.core.caches.image_cache import ImageCache
41
from sickrage.core.common import Quality, SKIPPED, WANTED, UNKNOWN, DOWNLOADED, IGNORED, SNATCHED, SNATCHED_PROPER, UNAIRED, ARCHIVED, statusStrings, \
42
43
    SearchFormats
from sickrage.core.databases.main import MainDB, TVShowSchema, IMDbInfoSchema, BlacklistSchema, WhitelistSchema
44
from sickrage.core.exceptions import ShowNotFoundException, EpisodeNotFoundException, EpisodeDeletedException, MultipleEpisodesInDatabaseException
echel0n's avatar
echel0n committed
45
from sickrage.core.helpers import list_media_files, is_media_file, try_int, safe_getattr
46
from sickrage.core.media.util import showImage
47
from sickrage.core.tv.episode import TVEpisode
48
from sickrage.indexers import IndexerApi
49
from sickrage.indexers.exceptions import indexer_attributenotfound, indexer_exception
50
51


52
class TVShow(object):
53
    def __init__(self, indexer_id, indexer, lang='en', location=''):
54
        self.lock = threading.Lock()
55
        self._episodes = {}
56

57
58
59
60
61
        with sickrage.app.main_db.session() as session:
            try:
                query = session.query(MainDB.TVShow).filter_by(indexer_id=indexer_id, indexer=indexer).one()
                self._data_local = query.as_dict()
            except orm.exc.NoResultFound:
62
63
                self._data_local = MainDB.TVShow().as_dict()
                self._data_local.update(**{
64
65
66
67
                    'indexer_id': indexer_id,
                    'indexer': indexer,
                    'lang': lang,
                    'location': location
68
                })
69

70
                self.load_from_indexer()
71

72
73
        sickrage.app.shows.update({(self.indexer_id, self.indexer): self})

74
75
    @property
    def indexer_id(self):
76
        return self._data_local['indexer_id']
77
78
79

    @indexer_id.setter
    def indexer_id(self, value):
80
        self._data_local['indexer_id'] = value
81
82
83

    @property
    def indexer(self):
84
        return self._data_local['indexer']
85
86
87

    @indexer.setter
    def indexer(self, value):
88
        self._data_local['indexer'] = value
89
90
91

    @property
    def name(self):
92
        return self._data_local['name']
93
94
95

    @name.setter
    def name(self, value):
96
        self._data_local['name'] = value
97
98
99

    @property
    def location(self):
100
        return self._data_local['location']
101
102
103

    @location.setter
    def location(self, value):
104
        self._data_local['location'] = value
105
106
107

    @property
    def network(self):
108
        return self._data_local['network']
109
110
111

    @network.setter
    def network(self, value):
112
        self._data_local['network'] = value
113
114
115

    @property
    def genre(self):
116
        return self._data_local['genre']
117
118
119

    @genre.setter
    def genre(self, value):
120
        self._data_local['genre'] = value
121
122
123

    @property
    def overview(self):
124
        return self._data_local['overview']
125
126
127

    @overview.setter
    def overview(self, value):
128
        self._data_local['overview'] = value
129
130
131

    @property
    def classification(self):
132
        return self._data_local['classification']
133
134
135

    @classification.setter
    def classification(self, value):
136
        self._data_local['classification'] = value
137
138
139

    @property
    def runtime(self):
140
        return self._data_local['runtime']
141
142
143

    @runtime.setter
    def runtime(self, value):
144
        self._data_local['runtime'] = value
145
146
147

    @property
    def quality(self):
148
        return self._data_local['quality']
149
150
151

    @quality.setter
    def quality(self, value):
152
        self._data_local['quality'] = value
153
154
155

    @property
    def airs(self):
156
        return self._data_local['airs']
157
158
159

    @airs.setter
    def airs(self, value):
160
        self._data_local['airs'] = value
161
162
163

    @property
    def status(self):
164
        return self._data_local['status']
165
166
167

    @status.setter
    def status(self, value):
168
        self._data_local['status'] = value
169
170
171

    @property
    def flatten_folders(self):
172
        return self._data_local['flatten_folders']
173
174
175

    @flatten_folders.setter
    def flatten_folders(self, value):
176
        self._data_local['flatten_folders'] = value
177
178
179

    @property
    def paused(self):
180
        return self._data_local['paused']
181
182
183

    @paused.setter
    def paused(self, value):
184
        self._data_local['paused'] = value
185

186
187
188
189
190
191
192
193
    @property
    def scene(self):
        return self._data_local['scene']

    @scene.setter
    def scene(self, value):
        self._data_local['scene'] = value

194
195
    @property
    def anime(self):
196
        return self._data_local['anime']
197
198
199

    @anime.setter
    def anime(self, value):
200
        self._data_local['anime'] = value
201
202

    @property
203
204
    def search_format(self):
        return self._data_local['search_format']
205

206
207
208
    @search_format.setter
    def search_format(self, value):
        self._data_local['search_format'] = value
209
210
211

    @property
    def subtitles(self):
212
        return self._data_local['subtitles']
213
214
215

    @subtitles.setter
    def subtitles(self, value):
216
        self._data_local['subtitles'] = value
217
218
219

    @property
    def dvdorder(self):
220
        return self._data_local['dvdorder']
221
222
223

    @dvdorder.setter
    def dvdorder(self, value):
224
        self._data_local['dvdorder'] = value
225
226
227

    @property
    def skip_downloaded(self):
228
        return self._data_local['skip_downloaded']
229
230
231

    @skip_downloaded.setter
    def skip_downloaded(self, value):
232
        self._data_local['skip_downloaded'] = value
233
234
235

    @property
    def startyear(self):
236
        return self._data_local['startyear']
237
238
239

    @startyear.setter
    def startyear(self, value):
240
        self._data_local['startyear'] = value
241
242
243

    @property
    def lang(self):
244
        return self._data_local['lang']
245
246
247

    @lang.setter
    def lang(self, value):
248
        self._data_local['lang'] = value
249
250
251

    @property
    def imdb_id(self):
252
        return self._data_local['imdb_id']
253
254
255

    @imdb_id.setter
    def imdb_id(self, value):
256
        self._data_local['imdb_id'] = value
257
258
259

    @property
    def rls_ignore_words(self):
260
        return self._data_local['rls_ignore_words']
261
262
263

    @rls_ignore_words.setter
    def rls_ignore_words(self, value):
264
        self._data_local['rls_ignore_words'] = value
265
266
267

    @property
    def rls_require_words(self):
268
        return self._data_local['rls_require_words']
269
270
271

    @rls_require_words.setter
    def rls_require_words(self, value):
272
        self._data_local['rls_require_words'] = value
273
274
275

    @property
    def default_ep_status(self):
276
        return self._data_local['default_ep_status']
277
278
279

    @default_ep_status.setter
    def default_ep_status(self, value):
280
        self._data_local['default_ep_status'] = value
281
282
283

    @property
    def sub_use_sr_metadata(self):
284
        return self._data_local['sub_use_sr_metadata']
285
286
287

    @sub_use_sr_metadata.setter
    def sub_use_sr_metadata(self, value):
288
        self._data_local['sub_use_sr_metadata'] = value
289
290
291

    @property
    def notify_list(self):
292
        return self._data_local['notify_list']
293
294
295

    @notify_list.setter
    def notify_list(self, value):
296
        self._data_local['notify_list'] = value
297
298
299

    @property
    def search_delay(self):
300
        return self._data_local['search_delay']
301
302
303

    @search_delay.setter
    def search_delay(self, value):
304
        self._data_local['search_delay'] = value
305

306
307
    @property
    def scene_exceptions(self):
308
309
310
        if self._data_local['scene_exceptions']:
            return list(filter(None, self._data_local['scene_exceptions'].split(',')))
        return []
311
312
313
314
315

    @scene_exceptions.setter
    def scene_exceptions(self, value):
        self._data_local['scene_exceptions'] = ','.join(value)

316
317
    @property
    def last_update(self):
318
        return self._data_local['last_update']
319
320
321

    @last_update.setter
    def last_update(self, value):
322
        self._data_local['last_update'] = value
323
324
325

    @property
    def last_refresh(self):
326
        return self._data_local['last_refresh']
327
328
329

    @last_refresh.setter
    def last_refresh(self, value):
330
        self._data_local['last_refresh'] = value
331
332
333

    @property
    def last_backlog_search(self):
334
        return self._data_local['last_backlog_search']
335
336
337

    @last_backlog_search.setter
    def last_backlog_search(self, value):
338
        self._data_local['last_backlog_search'] = value
339
340
341

    @property
    def last_proper_search(self):
342
        return self._data_local['last_proper_search']
343
344
345

    @last_proper_search.setter
    def last_proper_search(self, value):
346
        self._data_local['last_proper_search'] = value
347

348
349
350
351
352
353
354
355
    @property
    def last_scene_exceptions_refresh(self):
        return self._data_local['last_scene_exceptions_refresh']

    @last_scene_exceptions_refresh.setter
    def last_scene_exceptions_refresh(self, value):
        self._data_local['last_scene_exceptions_refresh'] = value

356
357
358
359
360
361
362
363
    @property
    def last_xem_refresh(self):
        return self._data_local['last_xem_refresh']

    @last_xem_refresh.setter
    def last_xem_refresh(self, value):
        self._data_local['last_xem_refresh'] = value

364
    @property
365
    def episodes(self):
366
        if not self._episodes:
367
            with sickrage.app.main_db.session() as session:
368
369
370
                for x in session.query(MainDB.TVEpisode).filter_by(showid=self.indexer_id, indexer=self.indexer):
                    self._episodes[x.indexer_id] = TVEpisode(showid=x.showid, indexer=x.indexer, season=x.season, episode=x.episode)
        return list(self._episodes.values())
371
372
373

    @property
    def imdb_info(self):
374
        with sickrage.app.main_db.session() as session:
375
            return session.query(MainDB.IMDbInfo).filter_by(indexer_id=self.indexer_id).one_or_none()
376

377
378
    @property
    def is_anime(self):
echel0n's avatar
echel0n committed
379
        return int(self.anime) > 0
380

381
382
383
    @property
    def airs_next(self):
        _airs_next = datetime.date.min
384
385

        with sickrage.app.main_db.session() as session:
386
387
388
389
390
391
392
393
394
395
396
397
398
399
            query = session.query(
                MainDB.TVEpisode
            ).filter_by(
                showid=self.indexer_id, indexer=self.indexer
            ).filter(
                MainDB.TVEpisode.season > 0,
                MainDB.TVEpisode.airdate >= datetime.date.today(),
                MainDB.TVEpisode.status.in_([UNAIRED, WANTED])
            ).order_by(
                MainDB.TVEpisode.airdate
            ).first()

            if query:
                _airs_next = query.airdate
400

401
402
403
404
405
        return _airs_next

    @property
    def airs_prev(self):
        _airs_prev = datetime.date.min
406
407

        with sickrage.app.main_db.session() as session:
408
409
410
411
412
413
414
415
416
417
418
419
420
421
            query = session.query(
                MainDB.TVEpisode
            ).filter_by(
                showid=self.indexer_id, indexer=self.indexer
            ).filter(
                MainDB.TVEpisode.season > 0,
                MainDB.TVEpisode.airdate < datetime.date.today(),
                MainDB.TVEpisode.status != UNAIRED
            ).order_by(
                sqlalchemy.desc(MainDB.TVEpisode.airdate)
            ).first()

            if query:
                _airs_prev = query.airdate
422

423
424
        return _airs_prev

425
426
    @property
    def episodes_unaired(self):
427
        with sickrage.app.main_db.session() as session:
428
429
430
431
432
433
434
435
436
437
            query = session.query(
                MainDB.TVEpisode.season, MainDB.TVEpisode.status
            ).filter_by(
                showid=self.indexer_id, indexer=self.indexer
            ).filter(
                MainDB.TVEpisode.status == UNAIRED
            )

            if not sickrage.app.config.display_show_specials:
                query = query.filter(MainDB.TVEpisode.season > 0)
438

439
        return query.count()
440

441
442
    @property
    def episodes_snatched(self):
443
        with sickrage.app.main_db.session() as session:
444
445
446
447
448
449
450
451
452
453
            query = session.query(
                MainDB.TVEpisode.season, MainDB.TVEpisode.status
            ).filter_by(
                showid=self.indexer_id, indexer=self.indexer
            ).filter(
                MainDB.TVEpisode.status.in_(Quality.SNATCHED + Quality.SNATCHED_BEST + Quality.SNATCHED_PROPER)
            )

            if not sickrage.app.config.display_show_specials:
                query = query.filter(MainDB.TVEpisode.season > 0)
454

455
        return query.count()
456
457
458

    @property
    def episodes_downloaded(self):
459
        with sickrage.app.main_db.session() as session:
460
461
462
463
464
465
466
            query = session.query(
                MainDB.TVEpisode.season, MainDB.TVEpisode.status
            ).filter_by(
                showid=self.indexer_id, indexer=self.indexer
            ).filter(
                MainDB.TVEpisode.status.in_(Quality.DOWNLOADED + Quality.ARCHIVED)
            )
467

468
469
            if not sickrage.app.config.display_show_specials:
                query = query.filter(MainDB.TVEpisode.season > 0)
470

471
        return query.count()
472
473

    @property
474
    def episodes_special(self):
475
        with sickrage.app.main_db.session() as session:
476
477
478
479
480
481
482
            query = session.query(
                MainDB.TVEpisode.season
            ).filter_by(
                showid=self.indexer_id, indexer=self.indexer
            ).filter(
                MainDB.TVEpisode.season == 0
            )
483

484
        return query.count()
485
486

    @property
487
    def episodes_total(self):
488
        with sickrage.app.main_db.session() as session:
489
490
491
492
493
494
495
            query = session.query(
                MainDB.TVEpisode.season, MainDB.TVEpisode.status
            ).filter_by(
                showid=self.indexer_id, indexer=self.indexer
            ).filter(
                MainDB.TVEpisode.status != UNAIRED
            )
496

497
498
            if not sickrage.app.config.display_show_specials:
                query = query.filter(MainDB.TVEpisode.season > 0)
499

500
        return query.count()
501

502
503
504
505
506
507
508
    @property
    def new_episodes(self):
        cur_date = datetime.date.today()
        cur_date += datetime.timedelta(days=1)
        cur_time = datetime.datetime.now(sickrage.app.tz)

        new_episodes = []
509
        for episode_object in self.episodes:
510
            if episode_object.status != UNAIRED or episode_object.season == 0 or not episode_object.airdate > datetime.date.min:
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
                continue

            air_date = episode_object.airdate
            air_date += datetime.timedelta(days=episode_object.show.search_delay)
            if not cur_date >= air_date:
                continue

            if episode_object.show.airs and episode_object.show.network:
                # This is how you assure it is always converted to local time
                air_time = sickrage.app.tz_updater.parse_date_time(episode_object.airdate,
                                                                   episode_object.show.airs,
                                                                   episode_object.show.network).astimezone(sickrage.app.tz)

                # filter out any episodes that haven't started airing yet,
                # but set them to the default status while they are airing
                # so they are snatched faster
                if air_time > cur_time:
                    continue

            new_episodes += [episode_object]

        return new_episodes

534
535
536
    @property
    def total_size(self):
        _total_size = 0
537
        _related_episodes = set()
538
        for episode_object in self.episodes:
539
540
541
            [_related_episodes.add(related_episode.indexer_id) for related_episode in episode_object.related_episodes]
            if episode_object.indexer_id not in _related_episodes:
                _total_size += episode_object.file_size
542
543
        return _total_size

544
545
    @property
    def network_logo_name(self):
echel0n's avatar
echel0n committed
546
        return unidecode(self.network).lower()
547

548
    @property
549
550
    def release_groups(self):
        if self.is_anime:
551
            return BlackAndWhiteList(self.indexer_id)
552

553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
    @property
    def poster(self):
        return showImage(self.indexer_id, 'poster').url

    @property
    def banner(self):
        return showImage(self.indexer_id, 'banner').url

    @property
    def allowed_qualities(self):
        allowed_qualities, __ = Quality.split_quality(self.quality)
        return allowed_qualities

    @property
    def preferred_qualities(self):
        __, preferred_qualities = Quality.split_quality(self.quality)
        return preferred_qualities

    @property
    def show_queue_status(self):
        if sickrage.app.show_queue.is_being_added(self.indexer_id):
            return {
                'action': 'ADD',
                'message': _('This show is in the process of being downloaded - the info below is incomplete.')
            }
578
579
580
581
582
        elif sickrage.app.show_queue.is_being_removed(self.indexer_id):
            return {
                'action': 'REMOVE',
                'message': _('This show is in the process of being removed.')
            }
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
        elif sickrage.app.show_queue.is_being_updated(self.indexer_id):
            return {
                'action': 'UPDATE',
                'message': _('The information on this page is in the process of being updated.')
            }
        elif sickrage.app.show_queue.is_being_refreshed(self.indexer_id):
            return {
                'action': 'REFRESH',
                'message': _('The episodes below are currently being refreshed from disk')
            }
        elif sickrage.app.show_queue.is_being_subtitled(self.indexer_id):
            return {
                'action': 'SUBTITLE',
                'message': _('Currently downloading subtitles for this show')
            }
        elif sickrage.app.show_queue.is_queued_to_refresh(self.indexer_id):
            return {
                'action': 'REFRESH_QUEUE',
                'message': _('This show is queued to be refreshed.')
            }
        elif sickrage.app.show_queue.is_queued_to_update(self.indexer_id):
            return {
                'action': 'UPDATE_QUEUE',
                'message': _('This show is queued and awaiting an update.')
            }
        elif sickrage.app.show_queue.is_queued_to_subtitle(self.indexer_id):
            return {
                'action': 'SUBTITLE_QUEUE',
                'message': _('This show is queued and awaiting subtitles download.')
            }

614
615
616
617
618
619
620
621
622
623
        return {}

    @property
    def is_loading(self):
        return self.show_queue_status.get('action') == 'ADD'

    @property
    def is_removing(self):
        return self.show_queue_status.get('action') == 'REMOVE'

624
    def save(self):
625
        with self.lock, sickrage.app.main_db.session() as session:
626
            sickrage.app.log.debug("{0:d}: Saving to database: {1}".format(self.indexer_id, self.name))
627
628
629

            try:
                query = session.query(MainDB.TVShow).filter_by(indexer_id=self.indexer_id, indexer=self.indexer).one()
630
                query.update(**self._data_local)
631
632
633
634
            except orm.exc.NoResultFound:
                session.add(MainDB.TVShow(**self._data_local))
            finally:
                session.commit()
635

636
    def delete(self):
637
        with self.lock, sickrage.app.main_db.session() as session:
638
639
            session.query(MainDB.TVShow).filter_by(indexer_id=self.indexer_id, indexer=self.indexer).delete()
            session.commit()
640

641
    def flush_episodes(self):
642
        self._episodes.clear()
643

644
    def load_from_indexer(self, cache=True, tvapi=None):
645
        sickrage.app.log.debug(str(self.indexer_id) + ": Loading show info from " + IndexerApi(self.indexer).name)
echel0n's avatar
echel0n committed
646

647
648
649
650
        t = tvapi
        if not t:
            lINDEXER_API_PARMS = IndexerApi(self.indexer).api_params.copy()
            lINDEXER_API_PARMS['cache'] = cache
echel0n's avatar
echel0n committed
651

652
            lINDEXER_API_PARMS['language'] = self.lang or sickrage.app.config.indexer_default_language
echel0n's avatar
echel0n committed
653

654
655
            if self.dvdorder != 0:
                lINDEXER_API_PARMS['dvdorder'] = True
echel0n's avatar
echel0n committed
656

657
            t = IndexerApi(self.indexer).indexer(**lINDEXER_API_PARMS)
echel0n's avatar
echel0n committed
658

659
660
661
        myEp = t[self.indexer_id]
        if not myEp:
            raise indexer_exception
echel0n's avatar
echel0n committed
662

663
664
665
666
        try:
            self.name = myEp['seriesname'].strip()
        except AttributeError:
            raise indexer_attributenotfound("Found %s, but attribute 'seriesname' was empty." % self.indexer_id)
echel0n's avatar
echel0n committed
667

668
669
670
671
672
673
        self.overview = safe_getattr(myEp, 'overview', self.overview)
        self.classification = safe_getattr(myEp, 'classification', self.classification)
        self.genre = safe_getattr(myEp, 'genre', self.genre)
        self.network = safe_getattr(myEp, 'network', self.network)
        self.runtime = try_int(safe_getattr(myEp, 'runtime', self.runtime))
        self.imdb_id = safe_getattr(myEp, 'imdbid', self.imdb_id)
echel0n's avatar
echel0n committed
674

675
676
677
678
        try:
            self.airs = (safe_getattr(myEp, 'airsdayofweek') + " " + safe_getattr(myEp, 'airstime')).strip()
        except:
            self.airs = ''
echel0n's avatar
echel0n committed
679

680
681
682
683
        try:
            self.startyear = try_int(str(safe_getattr(myEp, 'firstaired') or datetime.date.min).split('-')[0])
        except:
            self.startyear = 0
echel0n's avatar
echel0n committed
684

685
        self.status = safe_getattr(myEp, 'status', self.status)
686

687
        self.save()
echel0n's avatar
echel0n committed
688

689
    def load_episodes_from_indexer(self, cache=True):
690
691
692
693
694
695
696
697
698
699
700
701
        scanned_eps = {}

        l_indexer_api_parms = IndexerApi(self.indexer).api_params.copy()
        l_indexer_api_parms['cache'] = cache

        l_indexer_api_parms['language'] = self.lang or sickrage.app.config.indexer_default_language

        if self.dvdorder != 0:
            l_indexer_api_parms['dvdorder'] = True

        t = IndexerApi(self.indexer).indexer(**l_indexer_api_parms)

702
703
704
705
        sickrage.app.log.debug(str(self.indexer_id) + ": Loading all episodes from " + IndexerApi(self.indexer).name + "..")

        # flush episodes from cache so we can reload from database
        self.flush_episodes()
706

707
708
709
710
711
        indexer_data = t[self.indexer_id]
        if not indexer_data:
            raise indexer_exception

        for season in indexer_data:
712
            scanned_eps[season] = {}
713
            for episode in indexer_data[season]:
714
715
716
717
718
                # need some examples of wtf episode 0 means to decide if we want it or not
                if episode == 0:
                    continue

                try:
719
                    episode_obj = self.get_episode(season, episode)
720
                except EpisodeNotFoundException:
721
                    continue
722
723
724
                else:
                    try:
                        episode_obj.load_from_indexer(season, episode)
725
                        episode_obj.save()
726
727
728
                    except EpisodeDeletedException:
                        sickrage.app.log.info("The episode was deleted, skipping the rest of the load")
                        continue
729
730
731
732
733
734

                scanned_eps[season][episode] = True

        # Done updating save last update date
        self.last_update = datetime.date.today().toordinal()

735
        self.save()
736

737
        return scanned_eps
echel0n's avatar
echel0n committed
738

739
    def get_episode(self, season=None, episode=None, absolute_number=None, location=None, no_create=False):
740
        try:
741
            if season is None and episode is None and absolute_number is not None:
742
743
744
745
746
                with sickrage.app.main_db.session() as session:
                    query = session.query(MainDB.TVEpisode).filter_by(showid=self.indexer_id, indexer=self.indexer, absolute_number=absolute_number).one()
                    sickrage.app.log.debug("Found episode by absolute_number %s which is S%02dE%02d" % (absolute_number, query.season, query.episode))
                    season = query.season
                    episode = query.episode
747

748
            for tv_episode in self.episodes:
749
750
751
                if tv_episode.season == season and tv_episode.episode == episode:
                    return tv_episode
            else:
752
                if no_create:
753
                    return None
754
                tv_episode = TVEpisode(showid=self.indexer_id, indexer=self.indexer, season=season, episode=episode, location=location or '')
755
                self._episodes[tv_episode.indexer_id] = tv_episode
756
                return tv_episode
757
758
        except orm.exc.MultipleResultsFound:
            if absolute_number is not None:
759
                sickrage.app.log.debug("Multiple entries for absolute number: " + str(absolute_number) + " in show: " + self.name + " found ")
760
761
762
763
764
            raise MultipleEpisodesInDatabaseException
        except orm.exc.NoResultFound:
            if absolute_number is not None:
                sickrage.app.log.debug("No entries for absolute number: " + str(absolute_number) + " in show: " + self.name + " found.")
            raise EpisodeNotFoundException
765

766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
    def delete_episode(self, season, episode, full=False):
        episode_object = self.get_episode(season, episode, no_create=True)
        if not episode_object:
            return

        data = sickrage.app.notifier_providers['trakt'].trakt_episode_data_generate([(episode_object.season, episode_object.episode)])
        if sickrage.app.config.use_trakt and sickrage.app.config.trakt_sync_watchlist and data:
            sickrage.app.log.debug("Deleting episode from Trakt")
            sickrage.app.notifier_providers['trakt'].update_watchlist(self, data_episode=data, update="remove")

        if full and os.path.isfile(episode_object.location):
            sickrage.app.log.info('Attempt to delete episode file %s' % episode_object.location)

            try:
                os.remove(episode_object.location)
            except OSError as e:
                sickrage.app.log.warning('Unable to delete episode file %s: %s / %s' % (episode_object.location, repr(e), str(e)))

        # delete episode from show episode cache
        sickrage.app.log.debug("Deleting %s S%02dE%02d from the shows episode cache" % (self.name, episode_object.season or 0, episode_object.episode or 0))
        try:
            del self._episodes[episode_object.indexer_id]
        except KeyError:
            pass

        # delete episode from database
        sickrage.app.log.debug("Deleting %s S%02dE%02d from the DB" % (self.name, episode_object.season or 0, episode_object.episode or 0))
        episode_object.delete()

        raise EpisodeDeletedException()

797
    def write_show_nfo(self, force=False):
798
799
        result = False

800
        if not os.path.isdir(self.location):
801
            sickrage.app.log.info(str(self.indexer_id) + ": Show dir doesn't exist, skipping NFO generation")
802
803
            return False

804
        sickrage.app.log.debug(str(self.indexer_id) + ": Writing NFOs for show")
805
        for cur_provider in sickrage.app.metadata_providers.values():
806
            result = cur_provider.create_show_metadata(self, force) or result
807
808
809

        return result

810
    def write_metadata(self, show_only=False, force=False):
811
        if not os.path.isdir(self.location):
812
            sickrage.app.log.info(str(self.indexer_id) + ": Show dir doesn't exist, skipping NFO generation")
813
814
            return

815
        self.get_images()
816

817
        self.write_show_nfo(force)
818
819

        if not show_only:
820
            self.write_episode_nfos(force)
821
            self.update_episode_video_metadata()
822

823
    def write_episode_nfos(self, force=False):
824
        if not os.path.isdir(self.location):
825
            sickrage.app.log.info(str(self.indexer_id) + ": Show dir doesn't exist, skipping NFO generation")
826
827
            return

828
        sickrage.app.log.debug(str(self.indexer_id) + ": Writing NFOs for all episodes")
829

830
        for episode_obj in self.episodes:
831
            if episode_obj.location == '':
832
833
                continue

834
            sickrage.app.log.debug(str(self.indexer_id) + ": Retrieving/creating episode S%02dE%02d" % (episode_obj.season or 0, episode_obj.episode or 0))
echel0n's avatar
echel0n committed
835

836
            episode_obj.create_meta_files(force)
837

838
839
840
841
842
843
844
    def update_episode_video_metadata(self):
        if not os.path.isdir(self.location):
            sickrage.app.log.info(str(self.indexer_id) + ": Show dir doesn't exist, skipping video metadata updating")
            return

        sickrage.app.log.debug(str(self.indexer_id) + ": Updating video metadata for all episodes")

845
        for episode_obj in self.episodes:
846
847
848
            if episode_obj.location == '':
                continue

849
            sickrage.app.log.debug(str(self.indexer_id) + ": Updating video metadata for episode S%02dE%02d" % (episode_obj.season, episode_obj.episode))
850
851
852

            episode_obj.update_video_metadata()

853
    # find all media files in the show folder and create episodes for as many as possible
854
    def load_episodes_from_dir(self):
855
856
        from sickrage.core.nameparser import NameParser, InvalidNameException, InvalidShowException

857
        if not os.path.isdir(self.location):
858
            sickrage.app.log.debug(str(self.indexer_id) + ": Show dir doesn't exist, not loading episodes from disk")
859
860
            return

861
        sickrage.app.log.debug(str(self.indexer_id) + ": Loading all episodes from the show directory " + self.location)
862
863

        # get file list
864
        media_files = list_media_files(self.location)
865
866

        # create TVEpisodes from each media file (if possible)
867
        for mediaFile in media_files:
868
869
            curEpisode = None

870
            sickrage.app.log.debug(str(self.indexer_id) + ": Creating episode from " + mediaFile)
871
            try:
872
                curEpisode = self.make_ep_from_file(os.path.join(self.location, mediaFile))
873
            except (ShowNotFoundException, EpisodeNotFoundException) as e:
echel0n's avatar
echel0n committed
874
                sickrage.app.log.warning("Episode " + mediaFile + " returned an exception: {}".format(e))
875
            except EpisodeDeletedException:
876
                sickrage.app.log.debug("The episode deleted itself when I tried making an object for it")
877

878
879
            # skip to next episode?
            if not curEpisode:
880
881
882
883
884
885
886
                continue

            # see if we should save the release name in the db
            ep_file_name = os.path.basename(curEpisode.location)
            ep_file_name = os.path.splitext(ep_file_name)[0]

            try:
887
                parse_result = NameParser(False, show_id=self.indexer_id).parse(ep_file_name)
888
            except (InvalidNameException, InvalidShowException):
echel0n's avatar
echel0n committed
889
                parse_result = None
890

891
            if ' ' not in ep_file_name and parse_result and parse_result.release_group:
892
                sickrage.app.log.debug("Name " + ep_file_name + " gave release group of " + parse_result.release_group + ", seems valid")
893
                curEpisode.release_name = ep_file_name
894
                self.save()
895
896

            # store the reference in the show
897
            if self.subtitles and sickrage.app.config.use_subtitles:
898
                try:
899
                    curEpisode.refresh_subtitles()
900
                except Exception:
901
                    sickrage.app.log.error("%s: Could not refresh subtitles" % self.indexer_id)
902
                    sickrage.app.log.debug(traceback.format_exc())
903

904
905
906
907
908
909
910
911
    def load_imdb_info(self):
        imdb_info_mapper = {
            'imdbvotes': 'votes',
            'imdbrating': 'rating',
            'totalseasons': 'seasons',
            'imdbid': 'imdb_id'
        }

912
        if not re.search(r'^tt\d+$', self.imdb_id) and self.name:
913
914
            resp = sickrage.app.api.imdb.search_by_imdb_title(self.name)
            if not resp:
echel0n's avatar
echel0n committed
915
916
917
                resp = {}

            for x in resp.get('Search', []):
918
919
                try:
                    if int(x.get('Year'), 0) == self.startyear and x.get('Title') in self.name:
920
                        if re.search(r'^tt\d+$', x.get('imdbID', '')):
921
922
923
                            self.imdb_id = x.get('imdbID')
                            self.save()
                            break
924
                except Exception:
925
926
                    continue

927
        if re.search(r'^tt\d+$', self.imdb_id):
928
929
            sickrage.app.log.debug(str(self.indexer_id) + ": Obtaining IMDb info")

930
            imdb_info = sickrage.app.api.imdb.search_by_imdb_id(self.imdb_id)
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
            if not imdb_info:
                sickrage.app.log.debug(str(self.indexer_id) + ': Unable to obtain IMDb info')
                return

            imdb_info = dict((k.lower(), v) for k, v in imdb_info.items())
            for column in imdb_info.copy():
                if column in imdb_info_mapper:
                    imdb_info[imdb_info_mapper[column]] = imdb_info[column]

                if column not in MainDB.IMDbInfo.__table__.columns.keys():
                    del imdb_info[column]

            if not all([imdb_info.get('imdb_id'), imdb_info.get('votes'), imdb_info.get('rating'), imdb_info.get('genre')]):
                sickrage.app.log.debug(str(self.indexer_id) + ': IMDb info obtained does not meet our requirements')
                return

            sickrage.app.log.debug(str(self.indexer_id) + ": Obtained IMDb info ->" + str(imdb_info))

            # save imdb info to database
            imdb_info.update({
                'indexer_id': self.indexer_id,
                'last_update': datetime.date.today().toordinal()
            })

955
956
957
958
959
960
961
962
            with sickrage.app.main_db.session() as session:
                try:
                    dbData = session.query(MainDB.IMDbInfo).filter_by(indexer_id=self.indexer_id).one()
                    dbData.update(**imdb_info)
                except orm.exc.NoResultFound:
                    session.add(MainDB.IMDbInfo(**imdb_info))
                finally:
                    self.save()
963