__init__.py 57.1 KB
Newer Older
1
# Author: echel0n <[email protected]>
echel0n's avatar
echel0n committed
2
# URL: https://sickrage.ca
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#
# 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/>.

from __future__ import unicode_literals

21
import datetime
22
import os
echel0n's avatar
echel0n committed
23
import re
24
import threading
25
from collections import OrderedDict
26
from xml.etree.ElementTree import ElementTree
echel0n's avatar
echel0n committed
27

28
import sickrage
29
from sickrage.core.common import Quality, UNKNOWN, UNAIRED, statusStrings, dateTimeFormat, SKIPPED, NAMING_EXTEND, \
echel0n's avatar
echel0n committed
30
    NAMING_LIMITED_EXTEND, NAMING_LIMITED_EXTEND_E_PREFIXED, NAMING_DUPLICATE, NAMING_SEPARATED_REPEAT
31
from sickrage.core.exceptions import NoNFOException, \
echel0n's avatar
echel0n committed
32
    EpisodeNotFoundException, EpisodeDeletedException
33
from sickrage.core.helpers import isMediaFile, try_int, replaceExtension, \
echel0n's avatar
echel0n committed
34
35
    touchFile, sanitizeSceneName, remove_non_release_groups, remove_extension, sanitizeFileName, \
    safe_getattr, make_dirs, moveFile, delete_empty_folders
36
37
38
39
from sickrage.core.nameparser import NameParser, InvalidNameException, InvalidShowException
from sickrage.core.processors.post_processor import PostProcessor
from sickrage.core.scene_numbering import xem_refresh, get_scene_absolute_numbering, get_scene_numbering
from sickrage.core.updaters import tz_updater
40
from sickrage.indexers import srIndexerApi
41
from sickrage.indexers.exceptions import indexer_seasonnotfound, indexer_error, indexer_episodenotfound
42
from sickrage.notifiers import srNotifiers
echel0n's avatar
echel0n committed
43
44
from sickrage.subtitles import subtitle_extensions, download_subtitles, refresh_subtitles, subtitle_code_filter, \
    name_from_code
echel0n's avatar
echel0n committed
45

46
47

class TVEpisode(object):
echel0n's avatar
echel0n committed
48
    def __init__(self, show, season, episode, file=""):
49
50
51
        self.lock = threading.Lock()
        self.dirty = True

52
        self._name = ""
53
        self._indexer = int(show.indexer)
54
55
56
57
        self._season = season
        self._episode = episode
        self._absolute_number = 0
        self._description = ""
58
        self._subtitles = []
59
        self._subtitles_searchcount = 0
60
61
        self._subtitles_lastsearch = str(datetime.datetime.min)
        self._airdate = datetime.date.fromordinal(1)
62
63
64
65
66
        self._hasnfo = False
        self._hastbn = False
        self._status = UNKNOWN
        self._indexerid = 0
        self._file_size = 0
67
        self._release_name = ""
68
69
        self._is_proper = False
        self._version = 0
70
71
        self._release_group = ""
        self._location = file
72
73
74
75
76
77

        self.show = show
        self.scene_season = 0
        self.scene_episode = 0
        self.scene_absolute_number = 0

echel0n's avatar
echel0n committed
78
79
        self.populateEpisode(self.season, self.episode)

80
81
82
83
        self.relatedEps = []
        self.checkForMetaFiles()
        self.wantedQuality = []

84
85
    @property
    def name(self):
86
        return self._name or ""
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280

    @name.setter
    def name(self, value):
        if self._name != value:
            self.dirty = True
        self._name = value

    @property
    def season(self):
        return self._season

    @season.setter
    def season(self, value):
        if self._season != value:
            self.dirty = True
        self._season = value

    @property
    def episode(self):
        return self._episode

    @episode.setter
    def episode(self, value):
        if self._episode != value:
            self.dirty = True
        self._episode = value

    @property
    def absolute_number(self):
        return self._absolute_number

    @absolute_number.setter
    def absolute_number(self, value):
        if self._absolute_number != value:
            self.dirty = True
        self._absolute_number = value

    @property
    def description(self):
        return self._description

    @description.setter
    def description(self, value):
        if self._description != value:
            self.dirty = True
        self._description = value

    @property
    def subtitles(self):
        return self._subtitles

    @subtitles.setter
    def subtitles(self, value):
        if self._subtitles != value:
            self.dirty = True
        self._subtitles = value

    @property
    def subtitles_searchcount(self):
        return self._subtitles_searchcount

    @subtitles_searchcount.setter
    def subtitles_searchcount(self, value):
        if self._subtitles_searchcount != value:
            self.dirty = True
        self._subtitles_searchcount = value

    @property
    def subtitles_lastsearch(self):
        return self._subtitles_lastsearch

    @subtitles_lastsearch.setter
    def subtitles_lastsearch(self, value):
        if self._subtitles_lastsearch != value:
            self.dirty = True
        self._subtitles_lastsearch = value

    @property
    def airdate(self):
        return self._airdate

    @airdate.setter
    def airdate(self, value):
        if self._airdate != value:
            self.dirty = True
        self._airdate = value

    @property
    def hasnfo(self):
        return self._hasnfo

    @hasnfo.setter
    def hasnfo(self, value):
        if self._hasnfo != value:
            self.dirty = True
        self._hasnfo = value

    @property
    def hastbn(self):
        return self._hastbn

    @hastbn.setter
    def hastbn(self, value):
        if self._hastbn != value:
            self.dirty = True
        self._hastbn = value

    @property
    def status(self):
        return self._status

    @status.setter
    def status(self, value):
        if self._status != value:
            self.dirty = True
        self._status = value

    @property
    def indexer(self):
        return self._indexer

    @indexer.setter
    def indexer(self, value):
        if self._indexer != value:
            self.dirty = True
        self._indexer = value

    @property
    def indexerid(self):
        return self._indexerid

    @indexerid.setter
    def indexerid(self, value):
        if self._indexerid != value:
            self.dirty = True
        self._indexerid = value

    @property
    def file_size(self):
        return self._file_size

    @file_size.setter
    def file_size(self, value):
        if self._file_size != value:
            self.dirty = True
        self._file_size = value

    @property
    def release_name(self):
        return self._release_name

    @release_name.setter
    def release_name(self, value):
        if self._release_name != value:
            self.dirty = True
        self._release_name = value

    @property
    def is_proper(self):
        return self._is_proper

    @is_proper.setter
    def is_proper(self, value):
        if self._is_proper != value:
            self.dirty = True
        self._is_proper = value

    @property
    def version(self):
        return self._version

    @version.setter
    def version(self, value):
        if self._version != value:
            self.dirty = True
        self._version = value

    @property
    def release_group(self):
        return self._release_group

    @release_group.setter
    def release_group(self, value):
        if self._release_group != value:
            self.dirty = True
        self._release_group = value

    @property
    def location(self):
        return self._location

    @location.setter
    def location(self, new_location):
        if os.path.isfile(new_location):
echel0n's avatar
echel0n committed
281
            self.file_size = os.path.getsize(new_location)
282
            sickrage.srCore.srLogger.debug("{}: Episode location set to {}".format(self.show.indexerid, new_location))
283
284
            self.dirty = True
        self._location = new_location
285
286
287

    def refreshSubtitles(self):
        """Look for subtitles files and refresh the subtitles property"""
echel0n's avatar
echel0n committed
288
        self.subtitles, save_subtitles = refresh_subtitles(self)
289
290
291
        if save_subtitles:
            self.saveToDB()

292
    def downloadSubtitles(self):
293
        if not os.path.isfile(self.location):
294
            sickrage.srCore.srLogger.debug("%s: Episode file doesn't exist, can't download subtitles for S%02dE%02d" %
295
                                           (self.show.indexerid, self.season or 0, self.episode or 0))
296
297
            return

298
        sickrage.srCore.srLogger.debug(
echel0n's avatar
echel0n committed
299
            "%s: Downloading subtitles for S%02dE%02d" % (
300
301
                self.show.indexerid, self.season or 0, self.episode or 0))

echel0n's avatar
echel0n committed
302
        self.subtitles, newSubtitles = download_subtitles(self)
303

304
        self.subtitles_searchcount += 1 if self.subtitles_searchcount else 1
305
        self.subtitles_lastsearch = datetime.datetime.now().strftime(dateTimeFormat)
306
307
308
        self.saveToDB()

        if newSubtitles:
echel0n's avatar
echel0n committed
309
            subtitleList = ", ".join([name_from_code(newSub) for newSub in newSubtitles])
310
            sickrage.srCore.srLogger.debug("%s: Downloaded %s subtitles for S%02dE%02d" %
311
                                           (self.show.indexerid, subtitleList, self.season or 0, self.episode or 0))
312

echel0n's avatar
echel0n committed
313
            srNotifiers.notify_subtitle_download(self.prettyName(), subtitleList)
314
        else:
315
            sickrage.srCore.srLogger.debug("%s: No subtitles downloaded for S%02dE%02d" %
316
                                           (self.show.indexerid, self.season or 0, self.episode or 0))
317

318
319
        return newSubtitles

320
321
322
323
324
325
326
327
328
329
    def checkForMetaFiles(self):

        oldhasnfo = self.hasnfo
        oldhastbn = self.hastbn

        cur_nfo = False
        cur_tbn = False

        # check for nfo and tbn
        if os.path.isfile(self.location):
330
            for cur_provider in sickrage.srCore.metadataProvidersDict.values():
331
332
333
334
335
336
337
338
339
340
341
342
                if cur_provider.episode_metadata:
                    new_result = cur_provider._has_episode_metadata(self)
                else:
                    new_result = False
                cur_nfo = new_result or cur_nfo

                if cur_provider.episode_thumbnails:
                    new_result = cur_provider._has_episode_thumb(self)
                else:
                    new_result = False
                cur_tbn = new_result or cur_tbn

343
344
        self.hasnfo = cur_nfo
        self.hastbn = cur_tbn
345
346
347
348

        # if either setting has changed return true, if not return false
        return oldhasnfo != self.hasnfo or oldhastbn != self.hastbn

echel0n's avatar
echel0n committed
349
    def populateEpisode(self, season, episode):
350
        # attempt populating episode
echel0n's avatar
echel0n committed
351
352
353
354
355
        success = {'nfo': False,
                   'indexer': False,
                   'db': False}

        for method, func in OrderedDict([
echel0n's avatar
echel0n committed
356
            ('db', lambda: self.loadFromDB(season, episode)),
echel0n's avatar
echel0n committed
357
358
359
            ('nfo', lambda: self.loadFromNFO(self.location)),
            ('indexer', lambda: self.loadFromIndexer(season, episode)),
        ]).items():
360

echel0n's avatar
echel0n committed
361
            try:
362
                success[method] = func()
echel0n's avatar
echel0n committed
363
364
365
            except NoNFOException:
                sickrage.srCore.srLogger.error("%s: There was an error loading the NFO for episode S%02dE%02d" % (
                    self.show.indexerid, season or 0, episode or 0))
echel0n's avatar
echel0n committed
366
367
            except EpisodeDeletedException:
                pass
368

369
            # confirm if we successfully populated the episode
370
            if any(success.values()):
371
                return True
372

373
        # we failed to populate the episode
echel0n's avatar
echel0n committed
374
        raise EpisodeNotFoundException("Couldn't find episode S%02dE%02d" % (season or 0, episode or 0))
375
376

    def loadFromDB(self, season, episode):
377
        sickrage.srCore.srLogger.debug("%s: Loading episode details from DB for episode %s S%02dE%02d" % (
378
379
            self.show.indexerid, self.show.name, season or 0, episode or 0))

echel0n's avatar
echel0n committed
380
381
        dbData = [x['doc'] for x in
                  sickrage.srCore.mainDB.db.get_many('tv_episodes', self.show.indexerid, with_doc=True)
echel0n's avatar
echel0n committed
382
                  if x['doc']['season'] == season and x['doc']['episode'] == episode]
383

echel0n's avatar
echel0n committed
384
        if len(dbData) > 1:
echel0n's avatar
echel0n committed
385
            for ep in dbData:
echel0n's avatar
v8.7.9    
echel0n committed
386
                sickrage.srCore.mainDB.db.delete(ep)
echel0n's avatar
echel0n committed
387
            return False
echel0n's avatar
echel0n committed
388
        elif len(dbData) == 0:
389
            sickrage.srCore.srLogger.debug("%s: Episode S%02dE%02d not found in the database" % (
390
391
392
                self.show.indexerid, self.season or 0, self.episode or 0))
            return False
        else:
echel0n's avatar
echel0n committed
393
394
            self._season = season
            self._episode = episode
395
396
397
398
399
400
401
            self._name = dbData[0].get("name", self.name)
            self._absolute_number = dbData[0].get("absolute_number", self.absolute_number)
            self._description = dbData[0].get("description", self.description)
            self._subtitles = dbData[0].get("subtitles", self.subtitles).split(",")
            self._subtitles_searchcount = dbData[0].get("subtitles_searchcount", self.subtitles_searchcount)
            self._subtitles_lastsearch = dbData[0].get("subtitles_lastsearch", self.subtitles_lastsearch)
            self._airdate = datetime.date.fromordinal(int(dbData[0].get("airdate", self.airdate)))
402
            self._status = try_int(dbData[0]["status"], self.status)
403
            self.location = dbData[0].get("location", self.location)
404
405
406
            self._file_size = try_int(dbData[0]["file_size"], self.file_size)
            self._indexerid = try_int(dbData[0]["indexerid"], self.indexerid)
            self._indexer = try_int(dbData[0]["indexer"], self.indexer)
407
408
            self._release_name = dbData[0].get("release_name", self.release_name)
            self._release_group = dbData[0].get("release_group", self.release_group)
409
410
            self._is_proper = try_int(dbData[0]["is_proper"], self.is_proper)
            self._version = try_int(dbData[0]["version"], self.version)
411
412
413

            xem_refresh(self.show.indexerid, self.show.indexer)

414
415
416
            self.scene_season = try_int(dbData[0]["scene_season"], self.scene_season)
            self.scene_episode = try_int(dbData[0]["scene_episode"], self.scene_episode)
            self.scene_absolute_number = try_int(dbData[0]["scene_absolute_number"], self.scene_absolute_number)
417
418
419

            if self.scene_absolute_number == 0:
                self.scene_absolute_number = get_scene_absolute_numbering(
echel0n's avatar
echel0n committed
420
421
422
                    self.show.indexerid,
                    self.show.indexer,
                    self.absolute_number
423
424
425
426
                )

            if self.scene_season == 0 or self.scene_episode == 0:
                self.scene_season, self.scene_episode = get_scene_numbering(
echel0n's avatar
echel0n committed
427
428
429
                    self.show.indexerid,
                    self.show.indexer,
                    self.season, self.episode
430
431
432
433
434
                )

            return True

    def loadFromIndexer(self, season=None, episode=None, cache=True, tvapi=None, cachedSeason=None):
435
        indexer_name = srIndexerApi(self.indexer).name
436

437
438
        season = (self.season, season)[season is not None]
        episode = (self.episode, episode)[episode is not None]
439

440
        sickrage.srCore.srLogger.debug("{}: Loading episode details from {} for episode S{:02d}E{:02d}".format(
echel0n's avatar
echel0n committed
441
442
            self.show.indexerid, indexer_name, season or 0, episode or 0)
        )
443

444
        indexer_lang = self.show.lang or sickrage.srCore.srConfig.INDEXER_DEFAULT_LANGUAGE
445
446
447

        try:
            if cachedSeason is None:
448
449
                t = tvapi
                if not t:
450
                    lINDEXER_API_PARMS = srIndexerApi(self.indexer).api_params.copy()
451
                    lINDEXER_API_PARMS['cache'] = cache
452

453
                    lINDEXER_API_PARMS['language'] = indexer_lang
454
455

                    if self.show.dvdorder != 0:
456
                        lINDEXER_API_PARMS['dvdorder'] = True
457

458
                    t = srIndexerApi(self.indexer).indexer(**lINDEXER_API_PARMS)
459
460
461
462
463
                myEp = t[self.show.indexerid][season][episode]
            else:
                myEp = cachedSeason[episode]

        except (indexer_error, IOError) as e:
464
            sickrage.srCore.srLogger.debug("{} threw up an error: {}".format(indexer_name, e.message))
echel0n's avatar
echel0n committed
465

466
467
            # if the episode is already valid just log it, if not throw it up
            if self.name:
468
                sickrage.srCore.srLogger.debug(
echel0n's avatar
echel0n committed
469
                    "{} timed out but we have enough info from other sources, allowing the error".format(indexer_name))
470
                return False
471
            else:
472
                sickrage.srCore.srLogger.error("{} timed out, unable to create the episode".format(indexer_name))
473
                return False
echel0n's avatar
echel0n committed
474

475
        except (indexer_episodenotfound, indexer_seasonnotfound):
476
477
            sickrage.srCore.srLogger.debug(
                "Unable to find the episode on {}, has it been removed?".format(indexer_name))
echel0n's avatar
echel0n committed
478

479
480
481
            # if I'm no longer on the Indexers but I once was then delete myself from the DB
            if self.indexerid != -1:
                self.deleteEpisode()
482
            return False
483

484
        self.name = safe_getattr(myEp, 'episodename', self.name)
485
        if not myEp.get('episodename'):
486
487
488
            sickrage.srCore.srLogger.info(
                "This episode {} - S{:02d}E{:02d} has no name on {}. Setting to an empty string"
                    .format(self.show.name, season or 0, episode or 0, indexer_name))
echel0n's avatar
echel0n committed
489

490
        if not myEp.get('absolutenumber'):
491
            sickrage.srCore.srLogger.debug("This episode {} - S{:02d}E{:02d} has no absolute number on {}".format(
echel0n's avatar
echel0n committed
492
                self.show.name, season or 0, episode or 0, indexer_name))
493
        else:
494
            sickrage.srCore.srLogger.debug("{}: The absolute_number for S{:02d}E{:02d} is: {}".format(
495
                self.show.indexerid, season or 0, episode or 0, myEp["absolutenumber"]))
496
            self.absolute_number = try_int(safe_getattr(myEp, 'absolutenumber'), self.absolute_number)
497
498
499

        self.season = season
        self.episode = episode
500
501
502
503

        xem_refresh(self.show.indexerid, self.show.indexer)

        self.scene_absolute_number = get_scene_absolute_numbering(
echel0n's avatar
echel0n committed
504
505
506
            self.show.indexerid,
            self.show.indexer,
            self.absolute_number
507
508
509
        )

        self.scene_season, self.scene_episode = get_scene_numbering(
echel0n's avatar
echel0n committed
510
511
512
            self.show.indexerid,
            self.show.indexer,
            self.season, self.episode
513
514
        )

515
        self.description = safe_getattr(myEp, 'overview', self.description)
516

517
        firstaired = safe_getattr(myEp, 'firstaired') or datetime.date.fromordinal(1)
518
        try:
519
            rawAirdate = [int(x) for x in str(firstaired).split("-")]
520
            self.airdate = datetime.date(rawAirdate[0], rawAirdate[1], rawAirdate[2])
521
        except (ValueError, IndexError):
522
523
524
            sickrage.srCore.srLogger.warning(
                "Malformed air date of {} retrieved from {} for ({} - S{:02d}E{:02d})".format(
                    firstaired, indexer_name, self.show.name, season or 0, episode or 0))
525
526
527
528
529
530
            # if I'm incomplete on the indexer but I once was complete then just delete myself from the DB for now
            if self.indexerid != -1:
                self.deleteEpisode()
            return False

        # early conversion to int so that episode doesn't get marked dirty
531
        self.indexerid = try_int(safe_getattr(myEp, 'id'), self.indexerid)
532
        if self.indexerid is None:
533
            sickrage.srCore.srLogger.error("Failed to retrieve ID from " + srIndexerApi(self.indexer).name)
534
535
536
537
538
539
            if self.indexerid != -1:
                self.deleteEpisode()
            return False

        # don't update show status if show dir is missing, unless it's missing on purpose
        if not os.path.isdir(
540
541
                self.show.location) and not sickrage.srCore.srConfig.CREATE_MISSING_SHOW_DIRS and not sickrage.srCore.srConfig.ADD_SHOWS_WO_DIR:
            sickrage.srCore.srLogger.info(
542
                "The show dir %s is missing, not bothering to change the episode statuses since it'd probably be invalid" % self.show.location)
543
            return False
544
545

        if self.location:
546
            sickrage.srCore.srLogger.debug("%s: Setting status for S%02dE%02d based on status %s and location %s" %
547
548
                                           (self.show.indexerid, season or 0, episode or 0, statusStrings[self.status],
                                            self.location))
549
550

        if not os.path.isfile(self.location):
551
            if self.airdate >= datetime.date.today() or self.airdate == datetime.date.fromordinal(1):
552
                sickrage.srCore.srLogger.debug(
echel0n's avatar
echel0n committed
553
554
                    "Episode airs in the future or has no airdate, marking it %s" % statusStrings[
                        UNAIRED])
555
                self.status = UNAIRED
556
557
            elif self.status in [UNAIRED, UNKNOWN]:
                # Only do UNAIRED/UNKNOWN, it could already be snatched/ignored/skipped, or downloaded/archived to disconnected media
558
                sickrage.srCore.srLogger.debug(
559
                    "Episode has already aired, marking it %s" % statusStrings[self.show.default_ep_status])
560
                self.status = self.show.default_ep_status if self.season > 0 else SKIPPED  # auto-skip specials
561
            else:
562
                sickrage.srCore.srLogger.debug(
echel0n's avatar
echel0n committed
563
564
                    "Not touching status [ %s ] It could be skipped/ignored/snatched/archived" % statusStrings[
                        self.status])
565
566
567
568
569

        # if we have a media file then it's downloaded
        elif isMediaFile(self.location):
            # leave propers alone, you have to either post-process them or manually change them back
            if self.status not in Quality.SNATCHED_PROPER + Quality.DOWNLOADED + Quality.SNATCHED + Quality.ARCHIVED:
570
                sickrage.srCore.srLogger.debug(
echel0n's avatar
echel0n committed
571
572
                    "5 Status changes from " + str(self.status) + " to " + str(
                        Quality.statusFromName(self.location)))
573
                self.status = Quality.statusFromName(self.location, anime=self.show.is_anime)
574
575
576

        # shouldn't get here probably
        else:
577
            sickrage.srCore.srLogger.debug("6 Status changes from " + str(self.status) + " to " + str(UNKNOWN))
578
579
580
            self.status = UNKNOWN

        return True
581
582

    def loadFromNFO(self, location):
583
        if not os.path.isdir(self.show.location):
584
            sickrage.srCore.srLogger.info(
585
586
                "{}: The show dir is missing, not bothering to try loading the episode NFO".format(self.show.indexerid))
            return False
587

588
        sickrage.srCore.srLogger.debug(
589
            "{}: Loading episode details from the NFO file associated with {}".format(self.show.indexerid, location))
590

echel0n's avatar
echel0n committed
591
592
        if os.path.isfile(location):
            self.location = location
593
594
            if self.status == UNKNOWN:
                if isMediaFile(self.location):
595
                    sickrage.srCore.srLogger.debug("7 Status changes from " + str(self.status) + " to " + str(
echel0n's avatar
echel0n committed
596
                        Quality.statusFromName(self.location, anime=self.show.is_anime)))
597
                    self.status = Quality.statusFromName(self.location, anime=self.show.is_anime)
598
599

            nfoFile = replaceExtension(self.location, "nfo")
600
            sickrage.srCore.srLogger.debug(str(self.show.indexerid) + ": Using NFO name " + nfoFile)
601

602
            self.hasnfo = False
603
604
605
606
            if os.path.isfile(nfoFile):
                try:
                    showXML = ElementTree(file=nfoFile)
                except (SyntaxError, ValueError) as e:
607
                    sickrage.srCore.srLogger.error(
echel0n's avatar
echel0n committed
608
                        "Error loading the NFO, backing up the NFO and skipping for now: {}".format(e.message))
609
610
611
                    try:
                        os.rename(nfoFile, nfoFile + ".old")
                    except Exception as e:
612
                        sickrage.srCore.srLogger.error(
echel0n's avatar
echel0n committed
613
614
                            "Failed to rename your episode's NFO file - you need to delete it or fix it: {}".format(
                                e.message))
615
616
                    raise NoNFOException("Error in NFO format")

617
                for epDetails in showXML.iter('episodedetails'):
618
619
                    if epDetails.findtext('season') is None or int(
                            epDetails.findtext('season')) != self.season or epDetails.findtext(
echel0n's avatar
echel0n committed
620
                        'episode') is None or int(epDetails.findtext('episode')) != self.episode:
621
                        sickrage.srCore.srLogger.debug(
echel0n's avatar
echel0n committed
622
623
624
625
626
                            "%s: NFO has an <episodedetails> block for a different episode - wanted S%02dE%02d but got S%02dE%02d" %
                            (
                                self.show.indexerid, self.season or 0, self.episode or 0,
                                epDetails.findtext('season') or 0,
                                epDetails.findtext('episode') or 0))
627
628
629
630
631
                        continue

                    if epDetails.findtext('title') is None or epDetails.findtext('aired') is None:
                        raise NoNFOException("Error in NFO format (missing episode title or airdate)")

632
                    self.name = epDetails.findtext('title')
633
634
                    self.episode = try_int(epDetails.findtext('episode'))
                    self.season = try_int(epDetails.findtext('season'))
635
636
637
638

                    xem_refresh(self.show.indexerid, self.show.indexer)

                    self.scene_absolute_number = get_scene_absolute_numbering(
echel0n's avatar
echel0n committed
639
640
641
                        self.show.indexerid,
                        self.show.indexer,
                        self.absolute_number
642
643
644
                    )

                    self.scene_season, self.scene_episode = get_scene_numbering(
echel0n's avatar
echel0n committed
645
646
647
                        self.show.indexerid,
                        self.show.indexer,
                        self.season, self.episode
648
649
                    )

650
                    self.description = epDetails.findtext('plot') or self.description
651

652
                    self.airdate = datetime.date.fromordinal(1)
653
654
                    if epDetails.findtext('aired'):
                        rawAirdate = [int(x) for x in epDetails.findtext('aired').split("-")]
655
                        self.airdate = datetime.date(rawAirdate[0], rawAirdate[1], rawAirdate[2])
656

657
                    self.hasnfo = True
658

659
            self.hastbn = False
660
            if os.path.isfile(replaceExtension(nfoFile, "tbn")):
661
                self.hastbn = True
662

663
664
        return self.hasnfo

665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
    def __str__(self):
        toReturn = ""
        toReturn += "%r - S%02rE%02r - %r\n" % (self.show.name, self.season, self.episode, self.name)
        toReturn += "location: %r\n" % self.location
        toReturn += "description: %r\n" % self.description
        toReturn += "subtitles: %r\n" % ",".join(self.subtitles)
        toReturn += "subtitles_searchcount: %r\n" % self.subtitles_searchcount
        toReturn += "subtitles_lastsearch: %r\n" % self.subtitles_lastsearch
        toReturn += "airdate: %r (%r)\n" % (self.airdate.toordinal(), self.airdate)
        toReturn += "hasnfo: %r\n" % self.hasnfo
        toReturn += "hastbn: %r\n" % self.hastbn
        toReturn += "status: %r\n" % self.status
        return toReturn

    def createMetaFiles(self):

681
        if not os.path.isdir(self.show.location):
682
            sickrage.srCore.srLogger.info(
echel0n's avatar
echel0n committed
683
                str(self.show.indexerid) + ": The show dir is missing, not bothering to try to create metadata")
684
685
686
687
688
689
690
691
692
693
694
695
            return

        self.createNFO()
        self.createThumbnail()

        if self.checkForMetaFiles():
            self.saveToDB()

    def createNFO(self):

        result = False

696
        for cur_provider in sickrage.srCore.metadataProvidersDict.values():
697
698
699
700
701
702
703
704
            result = cur_provider.create_episode_metadata(self) or result

        return result

    def createThumbnail(self):

        result = False

705
        for cur_provider in sickrage.srCore.metadataProvidersDict.values():
706
707
708
709
710
711
            result = cur_provider.create_episode_thumb(self) or result

        return result

    def deleteEpisode(self, full=False):

712
        sickrage.srCore.srLogger.debug(
echel0n's avatar
echel0n committed
713
            "Deleting %s S%02dE%02d from the DB" % (self.show.name, self.season or 0, self.episode or 0))
714
715
716

        # remove myself from the show dictionary
        if self.show.getEpisode(self.season, self.episode, noCreate=True) == self:
717
            sickrage.srCore.srLogger.debug("Removing myself from my show's list")
718
719
720
            del self.show.episodes[self.season][self.episode]

        # delete myself from the DB
721
        sickrage.srCore.srLogger.debug("Deleting myself from the database")
722

echel0n's avatar
echel0n committed
723
724
        [sickrage.srCore.mainDB.db.delete(x['doc']) for x in
         sickrage.srCore.mainDB.db.get_many('tv_episodes', self.show.indexerid, with_doc=True)
echel0n's avatar
echel0n committed
725
         if x['doc']['season'] == self.season and x['doc']['episode'] == self.episode]
726

727
        data = sickrage.srCore.notifiersDict['trakt'].trakt_episode_data_generate([(self.season, self.episode)])
728
729
        if sickrage.srCore.srConfig.USE_TRAKT and sickrage.srCore.srConfig.TRAKT_SYNC_WATCHLIST and data:
            sickrage.srCore.srLogger.debug("Deleting myself from Trakt")
730
            sickrage.srCore.notifiersDict['trakt'].update_watchlist(self.show, data_episode=data, update="remove")
731

732
        if full and os.path.isfile(self.location):
733
            sickrage.srCore.srLogger.info('Attempt to delete episode file %s' % self.location)
734
            try:
735
                os.remove(self.location)
736
            except OSError as e:
737
                sickrage.srCore.srLogger.warning('Unable to delete %s: %s / %s' % (self.location, repr(e), str(e)))
738
739
740

        raise EpisodeDeletedException()

echel0n's avatar
echel0n committed
741
    def saveToDB(self, forceSave=False):
742
743
744
745
746
747
748
749
750
751
        """
        Saves this episode to the database if any of its data has been changed since the last save.

        forceSave: If True it will save to the database even if no data has been changed since the
                    last save (aka if the record is not dirty).
        """

        if not self.dirty and not forceSave:
            return

752
753
        sickrage.srCore.srLogger.debug("%i: Saving episode to database: %s" % (self.show.indexerid, self.name))

echel0n's avatar
echel0n committed
754
755
        tv_episode = {
            '_t': 'tv_episodes',
756
            "showid": self.show.indexerid,
echel0n's avatar
echel0n committed
757
758
            "season": self.season,
            "episode": self.episode,
759
760
            "scene_season": self.scene_season,
            "scene_episode": self.scene_episode,
echel0n's avatar
echel0n committed
761
762
763
764
            "indexerid": self.indexerid,
            "indexer": self.indexer,
            "name": self.name,
            "description": self.description,
765
            "subtitles": ",".join(self.subtitles),
echel0n's avatar
echel0n committed
766
767
768
769
770
771
772
773
774
775
776
            "subtitles_searchcount": self.subtitles_searchcount,
            "subtitles_lastsearch": self.subtitles_lastsearch,
            "airdate": self.airdate.toordinal(),
            "hasnfo": self.hasnfo,
            "hastbn": self.hastbn,
            "status": self.status,
            "location": self.location,
            "file_size": self.file_size,
            "release_name": self.release_name,
            "is_proper": self.is_proper,
            "absolute_number": self.absolute_number,
777
            "scene_absolute_number": self.scene_absolute_number,
echel0n's avatar
echel0n committed
778
779
780
781
782
            "version": self.version,
            "release_group": self.release_group
        }

        try:
echel0n's avatar
echel0n committed
783
            dbData = \
784
785
                [x['doc'] for x in sickrage.srCore.mainDB.db.get_many('tv_episodes', self.show.indexerid, with_doc=True)
                 if x['doc']['indexerid'] == self.indexerid][0]
786

echel0n's avatar
echel0n committed
787
            dbData.update(tv_episode)
echel0n's avatar
v8.7.9    
echel0n committed
788
            sickrage.srCore.mainDB.db.update(dbData)
789
        except:
echel0n's avatar
v8.7.9    
echel0n committed
790
            sickrage.srCore.mainDB.db.insert(tv_episode)
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812

    def fullPath(self):
        if self.location is None or self.location == "":
            return None
        else:
            return os.path.join(self.show.location, self.location)

    def createStrings(self, pattern=None):
        patterns = [
            '%S.N.S%SE%0E',
            '%S.N.S%0SE%E',
            '%S.N.S%SE%E',
            '%S.N.S%0SE%0E',
            '%SN S%SE%0E',
            '%SN S%0SE%E',
            '%SN S%SE%E',
            '%SN S%0SE%0E'
        ]

        strings = []
        if not pattern:
            for p in patterns:
echel0n's avatar
echel0n committed
813
                strings += [self._format_pattern(p)]
814
            return strings
echel0n's avatar
echel0n committed
815
        return self._format_pattern(pattern)
816
817
818
819
820
821
822
823
824
825

    def prettyName(self):
        """
        Returns the name of this episode in a "pretty" human-readable format. Used for logging
        and notifications and such.

        Returns: A string representing the episode's name and season/ep numbers
        """

        if self.show.anime and not self.show.scene:
echel0n's avatar
echel0n committed
826
            return self._format_pattern('%SN - %AB - %EN')
827
        elif self.show.air_by_date:
echel0n's avatar
echel0n committed
828
            return self._format_pattern('%SN - %AD - %EN')
829

echel0n's avatar
echel0n committed
830
        return self._format_pattern('%SN - %Sx%0E - %EN')
831
832
833
834
835
836

    def proper_path(self):
        """
        Figures out the path where this episode SHOULD live according to the renaming rules, relative from the show dir
        """

837
        anime_type = sickrage.srCore.srConfig.NAMING_ANIME
838
839
840
        if not self.show.is_anime:
            anime_type = 3

echel0n's avatar
echel0n committed
841
        result = self.formatted_filename(anime_type=anime_type)
842
843

        # if they want us to flatten it and we're allowed to flatten it then we will
844
        if self.show.flatten_folders and not sickrage.srCore.srConfig.NAMING_FORCE_FOLDERS:
845
846
847
848
            return result

        # if not we append the folder on and use that
        else:
echel0n's avatar
echel0n committed
849
            result = os.path.join(self.formatted_dir(), result)
850
851
852
853
854
855
856
857
858
859

        return result

    def rename(self):
        """
        Renames an episode file and all related files to the location and filename as specified
        in the naming settings.
        """

        if not os.path.isfile(self.location):
860
            sickrage.srCore.srLogger.warning(
echel0n's avatar
echel0n committed
861
                "Can't perform rename on " + self.location + " when it doesn't exist, skipping")
862
863
864
865
866
867
868
869
870
871
872
873
874
875
            return

        proper_path = self.proper_path()
        absolute_proper_path = os.path.join(self.show.location, proper_path)
        absolute_current_path_no_ext, file_ext = os.path.splitext(self.location)
        absolute_current_path_no_ext_length = len(absolute_current_path_no_ext)

        related_subs = []

        current_path = absolute_current_path_no_ext

        if absolute_current_path_no_ext.startswith(self.show.location):
            current_path = absolute_current_path_no_ext[len(self.show.location):]

876
        sickrage.srCore.srLogger.debug(
echel0n's avatar
echel0n committed
877
            "Renaming/moving episode from the base path " + self.location + " to " + absolute_proper_path)
878
879
880

        # if it's already named correctly then don't do anything
        if proper_path == current_path:
881
            sickrage.srCore.srLogger.debug(
echel0n's avatar
echel0n committed
882
                str(self.indexerid) + ": File " + self.location + " is already named correctly, skipping")
883
884
            return

echel0n's avatar
echel0n committed
885
886
        related_files = PostProcessor(self.location).list_associated_files(
            self.location, base_name_only=True, subfolders=True)
887
888

        # This is wrong. Cause of pp not moving subs.
889
        if self.show.subtitles and sickrage.srCore.srConfig.SUBTITLES_DIR != '':
echel0n's avatar
echel0n committed
890
            related_subs = PostProcessor(self.location).list_associated_files(
891
                sickrage.srCore.srConfig.SUBTITLES_DIR,
echel0n's avatar
echel0n committed
892
893
                subtitles_only=True,
                subfolders=True)
894
            absolute_proper_subs_path = os.path.join(sickrage.srCore.srConfig.SUBTITLES_DIR, self.formatted_filename())
895

896
        sickrage.srCore.srLogger.debug("Files associated to " + self.location + ": " + str(related_files))
897
898

        # move the ep file
echel0n's avatar
echel0n committed
899
        result = self.rename_ep_file(self.location, absolute_proper_path, absolute_current_path_no_ext_length)
900
901
902
903
904
905
906
907
908
909
910

        # move related files
        for cur_related_file in related_files:
            # We need to fix something here because related files can be in subfolders and the original code doesn't handle this (at all)
            cur_related_dir = os.path.dirname(os.path.abspath(cur_related_file))
            subfolder = cur_related_dir.replace(os.path.dirname(os.path.abspath(self.location)), '')
            # We now have a subfolder. We need to add that to the absolute_proper_path.
            # First get the absolute proper-path dir
            proper_related_dir = os.path.dirname(os.path.abspath(absolute_proper_path + file_ext))
            proper_related_path = absolute_proper_path.replace(proper_related_dir, proper_related_dir + subfolder)

echel0n's avatar
echel0n committed
911
            cur_result = self.rename_ep_file(cur_related_file, proper_related_path,
echel0n's avatar
echel0n committed
912
                                             absolute_current_path_no_ext_length + len(subfolder))
913
            if not cur_result:
914
                sickrage.srCore.srLogger.error(str(self.indexerid) + ": Unable to rename file " + cur_related_file)
915
916

        for cur_related_sub in related_subs:
917
            absolute_proper_subs_path = os.path.join(sickrage.srCore.srConfig.SUBTITLES_DIR, self.formatted_filename())
echel0n's avatar
echel0n committed
918
            cur_result = self.rename_ep_file(cur_related_sub, absolute_proper_subs_path,
echel0n's avatar
echel0n committed
919
                                             absolute_current_path_no_ext_length)
920
            if not cur_result:
921
                sickrage.srCore.srLogger.error(str(self.indexerid) + ": Unable to rename file " + cur_related_sub)
922
923
924
925
926
927
928
929
930
931
932
933

        # save the ep
        with self.lock:
            if result:
                self.location = absolute_proper_path + file_ext
                for relEp in self.relatedEps:
                    relEp.location = absolute_proper_path + file_ext

        # in case something changed with the metadata just do a quick check
        for curEp in [self] + self.relatedEps:
            curEp.checkForMetaFiles()

934
        # save any changes to the database
935
936
        with self.lock:
            for relEp in [self] + self.relatedEps:
echel0n's avatar
echel0n committed
937
                relEp.saveToDB()
938
939
940
941
942
943
944
945

    def airdateModifyStamp(self):
        """
        Make the modify date and time of a file reflect the show air date and time.
        Note: Also called from postProcessor

        """

946
947
        if not all([sickrage.srCore.srConfig.AIRDATE_EPISODES, self.airdate, self.location, self.show, self.show.airs,
                    self.show.network]): return
948

949
950
951
952
        try:
            airdate_ordinal = self.airdate.toordinal()
            if airdate_ordinal < 1:
                return
953

954
            airdatetime = tz_updater.parse_date_time(airdate_ordinal, self.show.airs, self.show.network)
955

956
957
            if sickrage.srCore.srConfig.FILE_TIMESTAMP_TIMEZONE == 'local':
                airdatetime = airdatetime.astimezone(tz_updater.sr_timezone)
958

959
960
            filemtime = datetime.datetime.fromtimestamp(os.path.getmtime(self.location)).replace(
                tzinfo=tz_updater.sr_timezone)
961

962
963
            if filemtime != airdatetime:
                import time
964

965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
                airdatetime = airdatetime.timetuple()
                sickrage.srCore.srLogger.debug(
                    str(self.show.indexerid) + ": About to modify date of '" + self.location +
                    "' to show air date " + time.strftime("%b %d,%Y (%H:%M)", airdatetime))
                try:
                    if touchFile(self.location, time.mktime(airdatetime)):
                        sickrage.srCore.srLogger.info(
                            str(self.show.indexerid) + ": Changed modify date of " + os.path.basename(self.location)
                            + " to show air date " + time.strftime("%b %d,%Y (%H:%M)", airdatetime))
                    else:
                        sickrage.srCore.srLogger.warning(
                            str(self.show.indexerid) + ": Unable to modify date of " + os.path.basename(
                                self.location)
                            + " to show air date " + time.strftime("%b %d,%Y (%H:%M)", airdatetime))
                except Exception:
980
                    sickrage.srCore.srLogger.warning(
981
982
983
984
985
                        str(self.show.indexerid) + ": Failed to modify date of '" + os.path.basename(self.location)
                        + "' to show air date " + time.strftime("%b %d,%Y (%H:%M)", airdatetime))
        except Exception:
            sickrage.srCore.srLogger.warning(
                "{}: Failed to modify date of '{}'".format(self.show.indexerid, os.path.basename(self.location)))
echel0n's avatar
echel0n committed
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000

    def _ep_name(self):
        """
        Returns the name of the episode to use during renaming. Combines the names of related episodes.
        Eg. "Ep Name (1)" and "Ep Name (2)" becomes "Ep Name"
            "Ep Name" and "Other Ep Name" becomes "Ep Name & Other Ep Name"
        """

        multiNameRegex = r"(.*) \(\d{1,2}\)"

        self.relatedEps = sorted(self.relatedEps, key=lambda x: x.episode)

        singleName = True
        curGoodName = None

For faster browsing, not all history is shown. View entire blame